国产xxxx99真实实拍_久久不雅视频_高清韩国a级特黄毛片_嗯老师别我我受不了了小说

資訊專欄INFORMATION COLUMN

你的Tree-Shaking并沒什么卵用

cncoder / 3160人閱讀

摘要:升級之后,項目的壓縮包并沒有什么明顯變化。這里可以參考下阮老師介紹的基本語法的循環是通過遍歷器迭代的,循環數組時并非是,然后通過下標尋值。樓主好奇為什么不能消除未引用的類。樓主我的代碼沒什么副作用啊。

本文將探討tree-shaking在當下(webpack@3, babel@6 以下)的現狀,以及研究為什么tree-shaking依舊舉步維艱的原因,最終總結當下能提高tree-shaking效果的一些手段。

Tree-Shaking這個名詞,很多前端coder已經耳熟能詳了,它代表的大意就是刪除沒用到的代碼。這樣的功能對于構建大型應用時是非常好的,因為日常開發經常需要引用各種庫。但大多時候僅僅使用了這些庫的某些部分,并非需要全部,此時Tree-Shaking如果能幫助我們刪除掉沒有使用的代碼,將會大大縮減打包后的代碼量。

Tree-Shaking在前端界由rollup首先提出并實現,后續webpack在2.x版本也借助于UglifyJS實現了。自那以后,在各類討論優化打包的文章中,都能看到Tree-Shaking的身影。

許多開發者看到就很開心,以為自己引用的elementUI、antd 等庫終于可以刪掉一大半了。然而理想是豐滿的,現實是骨干的。升級之后,項目的壓縮包并沒有什么明顯變化。

我也遇到了這樣的問題,前段時間,需要開發個組件庫。我非常納悶我開發的組件庫在打包后,為什么引用者通過ES6引用,最終依舊會把組件庫中沒有使用過的組件引入進來。

下面跟大家分享下,我在Tree-Shaking上的摸索歷程。

Tree-Shaking的原理

這里我不多冗余闡述,直接貼百度外賣前端的一篇文章:Tree-Shaking性能優化實踐 - 原理篇。

如果懶得看文章,可以看下如下總結:

ES6的模塊引入是靜態分析的,故而可以在編譯時正確判斷到底加載了什么代碼。

分析程序流,判斷哪些變量未被使用、引用,進而刪除此代碼。

很好,原理非常完美,那為什么我們的代碼又刪不掉呢?

先說原因:都是副作用的鍋!

副作用

了解過函數式編程的同學對副作用這詞肯定不陌生。它大致可以理解成:一個函數會、或者可能會對函數外部變量產生影響的行為。

舉個例子,比如這個函數:

function go (url) {
  window.location.href = url
}

這個函數修改了全局變量location,甚至還讓瀏覽器發生了跳轉,這就是一個有副作用的函數。

現在我們了解了副作用了,但是細想來,我寫的組件庫也沒有什么副作用啊,我每一個組件都是一個類,簡化一下,如下所示:

// componetns.js
export class Person {
  constructor ({ name, age, sex }) {
    this.className = "Person"
    this.name = name
    this.age = age
    this.sex = sex
  }
  getName () {
    return this.name
  }
}
export class Apple {
  constructor ({ model }) {
    this.className = "Apple"
    this.model = model
  }
  getModel () {
    return this.model
  }
}
// main.js
import { Apple } from "./components"

const appleModel = new Apple({
  model: "IphoneX"
}).getModel()

console.log(appleModel)

用rollup在線repl嘗試了下tree-shaking,也確實刪掉了Person,傳送門

可是為什么當我通過webpack打包組件庫,再被他人引入時,卻沒辦法消除未使用代碼呢?

因為我忽略了兩件事情:babel編譯 + webpack打包

成也Babel,敗也Babel

Babel不用我多解釋了,它能把ES6/ES7的代碼轉化成指定瀏覽器能支持的代碼。正是由于它,我們前端開發者才能有今天這樣美好的開發環境,能夠不用考慮瀏覽器兼容性地、暢快淋漓地使用最新的JavaScript語言特性。

然而也是由于它的編譯,一些我們原本看似沒有副作用的代碼,便轉化為了(可能)有副作用的。

如果懶得點開鏈接,可以看下Person類被babel編譯后的結果:

function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }

var _createClass = function() {
  function defineProperties(target, props) {
    for (var i = 0; i < props.length; i++) {
      var descriptor = props[i];
      descriptor.enumerable = descriptor.enumerable || !1, descriptor.configurable = !0,
      "value" in descriptor && (descriptor.writable = !0), Object.defineProperty(target, descriptor.key, descriptor);
    }
  }
  return function(Constructor, protoProps, staticProps) {
    return protoProps && defineProperties(Constructor.prototype, protoProps), staticProps && defineProperties(Constructor, staticProps),
    Constructor;
  };
}()

var Person = function () {
  function Person(_ref) {
    var name = _ref.name, age = _ref.age, sex = _ref.sex;
    _classCallCheck(this, Person);

    this.className = "Person";
    this.name = name;
    this.age = age;
    this.sex = sex;
  }

  _createClass(Person, [{
    key: "getName",
    value: function getName() {
      return this.name;
    }
  }]);
  return Person;
}();

我們的Person類被封裝成了一個IIFE(立即執行函數),然后返回一個構造函數。那它怎么就產生副作用了呢?問題就出現在_createClass這個方法上,你只要在上一個rollup的repl鏈接中,將Person的IIFE中的_createClass調用刪了,Person類就會被移除了。至于_createClass為什么會產生副作用,我們先放一邊。因為大家可能會產生另外一個疑問:Babel為什么要這樣去聲明構造函數的?

假如是我的話,我可能會這樣去編譯:

var Person = function () {
  function Person() {

  }
  Person.prototype.getName = function () { return this.name };
  return Person;
}();

因為我們以前就是這么寫“類”的,那babel為什么要采用Object.defineProperty這樣的形式呢,用原型鏈有什么不妥呢?自然是非常的不妥的,因為ES6的一些語法是有其特定的語義的。比如:

類內部聲明的方法,是不可枚舉的,而通過原型鏈聲明的方法是可以枚舉的。這里可以參考下阮老師介紹Class 的基本語法

for...of的循環是通過遍歷器(Iterator)迭代的,循環數組時并非是i++,然后通過下標尋值。這里依舊可以看下阮老師關于遍歷器與for...of的介紹,以及一篇babel關于for...of編譯的說明transform-es2015-for-of

所以,babel為了符合ES6真正的語義,編譯類時采取了Object.defineProperty來定義原型方法,于是導致了后續這些一系列問題。

眼尖的同學可能在我上述第二點中發的鏈接transform-es2015-for-of中看到,babel其實是有一個loose模式的,直譯的話叫做寬松模式。它是做什么用的呢?它會不嚴格遵循ES6的語義,而采取更符合我們平常編寫代碼時的習慣去編譯代碼。比如上述的Person類的屬性方法將會編譯成直接在原型鏈上聲明方法。

這個模式具體的babel配置如下:

// .babelrc
{
  "presets": [["env", { "loose": false }]]
}

同樣的,我放個在線repl示例方便大家直接查看效果:loose-mode

咦,如果我們真的不關心類方法能否被枚舉,開啟了loose模式,這樣是不是就沒有副作用產生,就能完美tree-shaking類了呢?

我們開啟了loose模式,使用rollup打包,發現還真是如此!傳送門

不夠屌的UglifyJS

然而不要開心的太早,當我們用Webpack配合UglifyJS打包文件時,這個Person類的IIFE又被打包進去了? What???

為了徹底搞明白這個問題,我搜到一條UglifyJS的issue:Class declaration in IIFE considered as side effect,仔細看了好久。對此有興趣、并且英語還ok的同學,可以快速去了解這條issue,還是挺有意思的。我大致闡述下這條issue下都說了些啥。

issue樓主-blacksonic 好奇為什么UglifyJS不能消除未引用的類。

UglifyJS貢獻者-kzc說,uglify不進行程序流分析,所以不能排除有可能有副作用的代碼。

樓主:我的代碼沒什么副作用啊。要不你們來個配置項,設置后,可以認為它是沒有副作用的,然后放心的刪了它們吧。

貢獻者:我們沒有程序流分析,我們干不了這事兒,實在想刪除他們,出門左轉 rollup 吼吧,他們屌,做了程序流分析,能判斷到底有沒有副作用。

樓主:遷移rollup成本有點高啊。我覺得加個配置不難啊,比如這樣這樣,巴拉巴拉。

貢獻者:歡迎提PR。

樓主:別嘛,你們項目上千行代碼,我咋提PR啊。我的代碼也沒啥副作用啊,您能詳細的說明下么?

貢獻者:變量賦值就是有可能產生副作用的!我舉個例子:

var V8Engine = (function () {
  function V8Engine () {}
  V8Engine.prototype.toString = function () { return "V8" }
  return V8Engine
}())
var V6Engine = (function () {
  function V6Engine () {}
  V6Engine.prototype = V8Engine.prototype // <---- side effect
  V6Engine.prototype.toString = function () { return "V6" }
  return V6Engine
}())
console.log(new V8Engine().toString())
貢獻者:V6Engine雖然沒有被使用,但是它修改了V8Engine原型鏈上的屬性,這就產生副作用了。你看rollup(樓主特意注明截至當時)目前就是這樣的策略,直接把V6Engine 給刪了,其實是不對的。

樓主以及一些路人甲乙丙丁,紛紛提出自己的建議與方案。最終定下,可以在代碼上通過/*@__PURE__*/這樣的注釋聲明此函數無副作用。

這個issue信息量比較大,也挺有意思,其中那位uglify貢獻者kzc,當時提出rollup存在的問題后還給rollup提了issue,rollup認為問題不大不緊急,這位貢獻者還順手給rollup提了個PR,解決了問題。。。

我再從這個issue中總結下幾點關鍵信息:

函數的參數若是引用類型,對于它屬性的操作,都是有可能會產生副作用的。因為首先它是引用類型,對它屬性的任何修改其實都是改變了函數外部的數據。其次獲取或修改它的屬性,會觸發getter或者setter,而getter、setter是不透明的,有可能會產生副作用。

uglify沒有完善的程序流分析。它可以簡單的判斷變量后續是否被引用、修改,但是不能判斷一個變量完整的修改過程,不知道它是否已經指向了外部變量,所以很多有可能會產生副作用的代碼,都只能保守的不刪除。

rollup有程序流分析的功能,可以更好的判斷代碼是否真正會產生副作用。

有的同學可能會想,連獲取對象的屬性也會產生副作用導致不能刪除代碼,這也太過分了吧!事實還真是如此,我再貼個示例演示一下:傳送門

代碼如下:

// maths.js
export function square ( x ) {
    return x.a
}
square({ a: 123 })

export function cube ( x ) {
    return x * x * x;
}
//main.js
import { cube } from "./maths.js";
console.log( cube( 5 ) ); // 125

打包結果如下:

function square ( x ) {
  return x.a
}
square({ a: 123 });

function cube ( x ) {
    return x * x * x;
}
console.log( cube( 5 ) ); // 125

而如果將square方法中的return x.a 改為 return x,則最終打包的結果則不會出現square方法。當然啦,如果不在maths.js文件中執行這個square方法,自然也是不會在打包文件中出現它的。

所以我們現在理解了,當時babel編譯成的_createClass方法為什么會有副作用。現在再回頭一看,它簡直渾身上下都是副作用。

查看uglify的具體配置,我們可以知道,目前uglify可以配置pure_getters: true來強制認為獲取對象屬性,是沒有副作用的。這樣可以通過它刪除上述示例中的square方法。不過由于沒有pure_setters這樣的配置,_createClass方法依舊被認為是有副作用的,無法刪除。

那到底該怎么辦?

聰明的同學肯定會想,既然babel編譯導致我們產生了副作用代碼,那我們先進行tree-shaking打包,最后再編譯bundle文件不就好了嘛。這確實是一個方案,然而可惜的是:這在處理項目自身資源代碼時是可行的,處理外部依賴npm包就不行了。因為人家為了讓工具包具有通用性、兼容性,大多是經過babel編譯的。而最占容量的地方往往就是這些外部依賴包。

那先從根源上討論,假如我們現在要開發一個組件庫提供給別人用,該怎么做?

如果是使用webpack打包JavaScript庫

先貼下webpack將項目打包為JS庫的文檔??梢钥吹絯ebpack有多種導出模式,一般大家都會選擇最具通用性的umd方式,但是webpack卻沒支持導出ES模塊的模式。

所以,假如你把所有的資源文件通過webpack打包到一個bundle文件里的話,那這個庫文件從此與Tree-shaking無緣。

那怎么辦呢?也不是沒有辦法。目前業界流行的組件庫多是將每一個組件或者功能函數,都打包成多帶帶的文件或目錄。然后可以像如下的方式引入:

import clone from "lodash/clone"

import Button from "antd/lib/button";

但是這樣呢也比較麻煩,而且不能同時引入多個組件。所以這些比較流行的組件庫大哥如antd,element專門開發了babel插件,使得用戶能以import { Button, Message } form "antd"這樣的方式去按需加載。本質上就是通過插件將上一句的代碼又轉化成如下:

import Button from "antd/lib/button";
import Message from "antd/lib/button";

這樣似乎是最完美的變相tree-shaking方案。唯一不足的是,對于組件庫開發者來說,需要專門開發一個babel插件;對于使用者來說,需要引入一個babel插件,稍微略增加了開發成本與使用成本。

除此之外,其實還有一個比較前沿的方法。是rollup的一個提案,在package.json中增加一個key:module,如下所示:

{
  "name": "my-package",
  "main": "dist/my-package.umd.js",
  "module": "dist/my-package.esm.js"
}

這樣,當開發者以es6模塊的方式去加載npm包時,會以module的值為入口文件,這樣就能夠同時兼容多種引入方式,(rollup以及webpack2+都已支持)。但是webpack不支持導出為es6模塊,所以webpack還是要拜拜。我們得上rollup!

(有人會好奇,那干脆把未打包前的資源入口文件暴露到module,讓使用者自己去編譯打包好了,那它就能用未編譯版的npm包進行tree-shaking了。這樣確實也不是不可以。但是,很多工程化項目的babel編譯配置,為了提高編譯速度,其實是會忽略掉node_modules內的文件的。所以為了保證這些同學的使用,我們還是應該要暴露出一份編譯過的ES6 Module。)

使用rollup打包JavaScript庫

吃了那么多虧后,我們終于明白,打包工具庫、組件庫,還是rollup好用,為什么呢?

它支持導出ES模塊的包。

它支持程序流分析,能更加正確的判斷項目本身的代碼是否有副作用。

我們只要通過rollup打出兩份文件,一份umd版,一份ES模塊版,它們的路徑分別設為main,module的值。這樣就能方便使用者進行tree-shaking。

那么問題又來了,使用者并不是用rollup打包自己的工程化項目的,由于生態不足以及代碼拆分等功能限制,一般還是用webpack做工程化打包。

使用webpack打包工程化項目

之前也提到了,我們可以先進行tree-shaking,再進行編譯,減少編譯帶來的副作用,從而增加tree-shaking的效果。那么具體應該怎么做呢?

首先我們需要去掉babel-loader,然后webpack打包結束后,再執行babel編譯文件。但是由于webpack項目常有多入口文件或者代碼拆分等需求,我們又需要寫一個配置文件,對應執行babel,這又略顯麻煩。所以我們可以使用webpack的plugin,讓這個環節依舊跑在webpack的打包流程中,就像uglifyjs-webpack-plugin一樣,不再是以loader的形式對單個資源文件進行操作,而是在打包最后的環節進行編譯。這里可能需要大家了解下webpack的plugin機制。

關于uglifyjs-webpack-plugin,這里有一個小細節,webpack默認會帶一個低版本的,可以直接用webpack.optimize.UglifyJsPlugin別名去使用。具體可以看webpack的相關說明

webpack =< v3.0.0 currently contains v0.4.6 of this plugin under webpack.optimize.UglifyJsPlugin as an alias. For usage of the latest version (v1.0.0), please follow the instructions below. Aliasing v1.0.0 as webpack.optimize.UglifyJsPlugin is scheduled for webpack v4.0.0

而這個低版本的uglifyjs-webpack-plugin使用的依賴uglifyjs也是低版本的,它沒有uglifyES6代碼的能力,故而如果我們有這樣的需求,需要在工程中重新npm install uglifyjs-webpack-plugin -D,安裝最新版本的uglifyjs-webpack-plugin,重新引入它并使用。

這樣之后,我們再使用webpack的babel插件進行編譯代碼。

問題又來了,這樣的需求比較少,因此webpack和babel官方都沒有這樣的插件,只有一個第三方開發者開發了一個插件babel-webpack-plugin。可惜的是這位作者已經近一年沒有維護這個插件了,并且存在著一個問題,此插件不會用項目根目錄下的.babelrc文件進行babel編譯。有人對此提了issue,卻也沒有任何回應。

那么又沒有辦法,就我來寫一個新的插件吧----webpack-babel-plugin,有了它之后我們就能讓webpack在最后打包文件之前進行babel編譯代碼了,具體如何安裝使用可以點開項目查看。注意這個配置需要在uglifyjs-webpack-plugin之后,像這樣:

plugins: [
  new UglifyJsPlugin(),
  new BabelPlugin()
]

但是這樣呢,有一個毛病,由于babel在最后階段去編譯比較大的文件,耗時比較長,所以建議區分下開發模式與生產模式。另外還有個更大的問題,webpack本身采用的編譯器acorn不支持對象的擴展運算符(...)以及某些還未正式成為ES標準的特性,所以。。。。。

所以如果特性用的非常超前,還是需要babel-loader,但是babel-loader要做專門的配置,把還在es stage階段的代碼編譯成ES2017的代碼,以便于webpack本身做處理。

感謝掘金熱心網友的提示,還有一個插件BabelMinifyWebpackPlugin,它所依賴的babel/minify也集成了uglifyjs。使用此插件便等同于上述使用UglifyJsPlugin + BabelPlugin的效果,如若有此方面需求,建議使用此插件。

總結

上面講了這么多,我最后再總結下,在當下階段,在tree-shaking上能夠盡力的事。

盡量不寫帶有副作用的代碼。諸如編寫了立即執行函數,在函數里又使用了外部變量等。

如果對ES6語義特性要求不是特別嚴格,可以開啟babel的loose模式,這個要根據自身項目判斷,如:是否真的要不可枚舉class的屬性。

如果是開發JavaScript庫,請使用rollup。并且提供ES6 module的版本,入口文件地址設置到package.json的module字段。

如果JavaScript庫開發中,難以避免的產生各種副作用代碼,可以將功能函數或者組件,打包成多帶帶的文件或目錄,以便于用戶可以通過目錄去加載。如有條件,也可為自己的庫開發多帶帶的webpack-loader,便于用戶按需加載。

如果是工程項目開發,對于依賴的組件,只能看組件提供者是否有對應上述3、4點的優化。對于自身的代碼,除1、2兩點外,對于項目有極致要求的話,可以先進行打包,最終再進行編譯。

如果對項目非常有把握,可以通過uglify的一些編譯配置,如:pure_getters: true,刪除一些強制認為不會產生副作用的代碼。

故而,在當下階段,依舊沒有比較簡單好用的方法,便于我們完整的進行tree-shaking。所以說,想做好一件事真難啊。不僅需要靠個人的努力,還需要考慮到歷史的進程。

PS: 此文中涉及到的代碼,我也傳到了github,可以點擊閱讀原文下載查看。

--閱讀原文

@丁香園F2E @相學長

--轉載請先經過本人授權。

文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。

轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/90805.html

相關文章

  • package.json 中的 Module 字段是干嘛的

    摘要:為何有查閱了的文檔,并沒有找到字段的定義,直到才知道它是中最早就提出的概念。況且目前大部分仍是采用,所以便使用了另一個字段。所以目前主流的打包工具都是支持的,鑒于其優點,字段很有可能加入的規范之中。 引入 最近團隊的一個同學在搞 npm library 源碼的調試插件,因為內部的一個組件庫含有大量的邏輯,在某個項目中不經意就出現一個磨人的 bug,但是組件庫發布都是打包編譯后的代碼,而...

    gnehc 評論0 收藏0
  • webpack4-Tree-Shaking優化

    摘要:概念由來已久,今天再來談一談,是因為中有了新的優化。簡單的介紹下什么是。它已經為我們消除了副作用。而且我并沒有引入。即便根據文件大小,可能還有朋友持懷疑態度。因為最近才接觸。所以沒有在低版本的時候打包過。 Tree-Shaking概念由來已久,今天再來談一談,是因為webpack4中有了新的優化。簡單的介紹下什么是Tree-Shaking。 代碼不會被執行 if(false) { ...

    bawn 評論0 收藏0
  • Webpack 中的 sideEffects 到底該怎么用?

    摘要:原文鏈接開始新增了一個特性,通過給加入聲明該包模塊是否包含副作用,從而可以為提供更大的優化空間。而通常我們期望的是模塊既然不被使用了,其中所有的代碼應該不被引入才對。但當我們加上之后,就能安全的把它從里完整的移除掉了。 原文鏈接 webpack v4 開始新增了一個 sideEffects 特性,通過給 package.json 加入 sideEffects: false 聲明該包模塊...

    googollee 評論0 收藏0
  • 我他喵的到底要怎樣才能在生產環境中用上 ES6 模塊化?

    摘要:因此,你還是需要各種各樣雜七雜八的工具來轉換你的代碼噢,我可去你媽的吧,這些東西都是干嘛的我就是想用個模塊化,我到底該用啥子本文正旨在列出幾種可用的在生產環境中放心使用模塊化的方法,希望能幫到諸位后來者這方面的中文資源實在是忒少了。 原文發表在我的博客上。最近搗鼓了一下 ES6 的模塊化,分享一些經驗 :) Python3 已經發布了九年了,Python 社區卻還在用 Python 2...

    KaltZK 評論0 收藏0
  • [譯]教程:如何使用Rollup打包JavaScript

    摘要:教程如何使用打包通過這個系列教程一步一步學習如何使用更小更快的取代和打包文件。安裝并且創建配置文件。提示是告訴我們實際需要哪些插件的集合。通過下面的命令安裝兩個插件更新然后,引入插件并添加進配置注意屬性是為了幫助模塊遷移到的一部分。 教程:如何使用Rollup打包JavaScript 通過這個系列教程一步一步學習如何使用更小更快的Rollup取代webpack和Browserify打包...

    luoyibu 評論0 收藏0

發表評論

0條評論

cncoder

|高級講師

TA的文章

閱讀更多
最新活動
閱讀需要支付1元查看
<