Tap 的英文單詞解釋,除了最經常使用的 點擊 手勢以外,還有一個意思是 水龍頭 —— 在 webpack 中指的是後一種;html
Webpack 能夠認爲是一種基於事件流的編程範例,內部的工做流程都是基於 插件 機制串接起來;webpack
而將這些插件粘合起來的就是webpack本身寫的基礎類 Tapable 是,plugin
方法就是該類暴露出來的;git
後面咱們將看到核心的對象 Compiler、Compilation 等都是繼承於該對象
基於該類規範而其的 Webpack 體系保證了插件的有序性,使得整個系統很是有彈性,擴展性很好;然而有一個致命的缺點就是調試、看源碼真是很痛苦,各類跳來跳去;(基於事件流的寫法,和程序語言中的 goto 語句很相似)github
把這個倉庫下載,使用 Webstorm 進行調試,test 目錄是很好的教程入口;web
Tapable.plugin():至關於把對象歸類到名爲 name 的對象下,以array的形式;全部的插件都存在私有變量 _plugin 變量中;shell
接下來咱們簡單節選幾個函數分析一下:express
該方法最普通也是最經常使用的,看一下它的定義:編程
Tapable.prototype.apply = function apply() { for(var i = 0; i < arguments.length; i++) { arguments[i].apply(this); } };
毫無懸念,就是 挨個順序 執行傳入到該函數方法中對象的 apply
方法;一般傳入該函數的對象也是 Tapable 插件 對象,所以必然也存在 apply
方法;(Webpack 的插件就是Tapable對象,所以必需要提供 apply
方法 )bootstrap
只是更改上下文爲當前 this
微信
所以當前這裏最大的做用就是傳入當前 Tapable 的上下文
// 模擬兩個插件 var _plugins = { "emit":[ function(a,b,cb){ setTimeout(()=>{ console.log('1',a,b); cb(); },1000); }, function(a,b,cb){ setTimeout(()=>{ console.log('2',a,b); cb(); },500) } ] } applyPluginsAsync("emit",'aaaa','bbbbb',function(){console.log('end')}); // 輸出結果: // 1 aaaa bbbbb // 2 aaaa bbbbb // end
咱們看到,雖然第一個插件是延後 1000ms 執行,第二個則是延後 500ms,但在真正執行的時候,是嚴格按照順序執行的;每一個插件須要在最後顯式調用cb()
通知下一個插件的運行;
這裏須要注意每一個插件的形參的個數都要一致,且最後一個必須是cb()方法,用於喚起下一個插件的運行;cb的第一個參數是err,若是該參數不爲空,就直接調用最後callback,中斷後續插件的運行;
大部分代碼和
applyPluginsAsync
有點兒相似
這個 applyPluginsParallel
主要功能和 最簡單的 applyPlugins
方法比較類似,不管如何都會讓全部註冊的插件運行一遍;
只是相比 applyPlugins
多了一個額外的功能,它最後 提供一個 callback 函數,這個 callback 的函數比較倔強,若是全部的插件x都正常執行,且最後都cb(),則會在最後執行callback裏的邏輯;不過,一旦其中某個插件運行出錯,就會調用這個callback(err),以後就算插件有錯誤也不會再調用該callback函數;
var _plugins = { "emit":[ function(a,b,cb){ setTimeout(()=>{ console.log('1',a,b); cb(null,'e222','33333'); },1000); }, function(a,b,cb){ setTimeout(()=>{ console.log('2',a,b); cb(null,'err'); },500) } ] } applyPluginsParallel("emit",'aaaa','bbbbb',function(a,b){console.log('end',a,b)}); // 輸出結果: // 2 aaaa bbbbb // 1 aaaa bbbbb // end undefined undefined
上面的兩個插件都是調用了 cb,且第一個參數是 null(表示沒有錯誤),因此最後能輸出 callback 函數中的 console 內容;
若是註釋兩個插件中任何一個 cb() 調用,你會發現最後的 callback 沒有執行;
若是讓 第二個 cb()的第一個值不是 null,好比 cb('err'),則 callback 以後輸出這個錯誤,以後不再會調用此 callback:
var _plugins = { "emit":[ function(a,b,cb){ setTimeout(()=>{ console.log('1',a,b); cb('e222','33333'); },1000); }, function(a,b,cb){ setTimeout(()=>{ console.log('2',a,b); cb('err'); },500) } ] } // 輸出結果: // 2 aaaa bbbbb // end err undefined // 1 aaaa bbbbb
顧名思義,這個方法至關因而 瀑布式 調用,給第一個插件傳入初始對象 init
,而後通過第一個插件調用以後會得到一個結果對象,該結果對象會傳給下一個插件 做爲初始值,直到最後調用完畢,最後一個插件的直接結果傳給 callback 做爲初始值;
這個方法應該是全部方法中最難理解的;
首先它的行爲和 applyPluginsParallel
很是類似,首先會 不管如何都會讓全部註冊的插件運行一遍(根據註冊的順序);
爲了讓 callback 執行,其前提條件是每一個插件都須要調用 cb();
但其中的 callback 只會執行一次(當傳給cb的值不是undefined/null 的時候),這一次執行順序是插件定義順序有關,而跟每一個插件中的 cb() 執行時間無關的;
var _plugins = { "emit":[ function(a,b,cb){ setTimeout(()=>{ console.log('1',a,b); cb(); },1000); }, function(a,b,cb){ setTimeout(()=>{ console.log('2',a,b); cb(); },500) }, function(a,b,cb){ setTimeout(()=>{ console.log('3',a,b); cb(); },1500) } ] } applyPluginsParallelBailResult("emit",'aaaa','bbbbb',function(a,b){console.log('end',a,b)}); // 運行結果 // 2 aaaa bbbbb // 1 aaaa bbbbb // 3 aaaa bbbbb // end undefined undefined
這是最普通的運行狀況,咱們稍微調整一下(注意三個插件運行的順序2-1-3),分別給cb傳入有效的值:
var _plugins = { "emit":[ function(a,b,cb){ setTimeout(()=>{ console.log('1',a,b); cb('1'); },1000); }, function(a,b,cb){ setTimeout(()=>{ console.log('2',a,b); cb('2'); },500) }, function(a,b,cb){ setTimeout(()=>{ console.log('3',a,b); cb('3'); },1500) } ] } applyPluginsParallelBailResult("emit",'aaaa','bbbbb',function(a,b){console.log('end',a,b)}); // 運行結果 // 2 aaaa bbbbb // 1 aaaa bbbbb // end 1 undefined // 3 aaaa bbbbb
能夠發現第1個插件 cb('1')
執行了,後續的 cb('2')
和 cb('3')
都給忽略了;
這是由於插件註冊順序是 1-2-3,雖然運行的時候順序是 2-1-3,但所運行的仍是 1 對應的 cb;因此,就算1執行的速度最慢(好比把其setTimeout的值設置成 2000),運行的 cb 仍然是1對應的cb;
其中涉及的魔法是 閉包,傳入的i
就是和註冊順序綁定了這樣一說明,你會發現
applyPluginsParallel
的 cb 執行時機是和執行時間有關係的,你能夠本身驗證一下;
總結一下,Tapable 就至關因而一個 事件管家,它所提供的 plugin
方法相似於 addEventListen
監聽事件,apply
方法相似於事件觸發函數 trigger
;
既然 Webpack 是基於 Tapable 搭建起來的,那麼咱們看一下 Webpack 構建一個模塊的基本事件流是如何的;
咱們在 Webpack 庫中的 Tapable.js 中每一個方法中新增 console
語句打出日誌,就能找出全部關鍵的事件名字:
打印結果:(這裏只列舉了簡單的事件流程,打包不一樣的入口文件會有所差別,但 事件出現的前後順序是固定的 )
類型 | 名字 | 事件名 |
---|---|---|
[C] | applyPluginsBailResult | entry-option |
[A] | applyPlugins | after-plugins |
[A] | applyPlugins | after-resolvers |
[A] | applyPlugins | environment |
[A] | applyPlugins | after-environment |
[D] | applyPluginsAsyncSeries | run |
[A] | applyPlugins | normal-module-factory |
[A] | applyPlugins | context-module-factory |
[A] | applyPlugins | compile |
[A] | applyPlugins | this-compilation |
[A] | applyPlugins | compilation |
[F] | applyPluginsParallel | make |
[E] | applyPluginsAsyncWaterfall | before-resolve |
[B] | applyPluginsWaterfall | factory |
[B] | applyPluginsWaterfall | resolver |
[A] | applyPlugins | resolve |
[A] | applyPlugins | resolve-step |
[G] | applyPluginsParallelBailResult | file |
[G] | applyPluginsParallelBailResult | directory |
[A] | applyPlugins | resolve-step |
[G] | applyPluginsParallelBailResult | result |
[E] | applyPluginsAsyncWaterfall | after-resolve |
[C] | applyPluginsBailResult | create-module |
[B] | applyPluginsWaterfall | module |
[A] | applyPlugins | build-module |
[A] | applyPlugins | normal-module-loader |
[C] | applyPluginsBailResult | program |
[C] | applyPluginsBailResult | statement |
[C] | applyPluginsBailResult | evaluate CallExpression |
[C] | applyPluginsBailResult | var data |
[C] | applyPluginsBailResult | evaluate Identifier |
[C] | applyPluginsBailResult | evaluate Identifier require |
[C] | applyPluginsBailResult | call require |
[C] | applyPluginsBailResult | evaluate Literal |
[C] | applyPluginsBailResult | call require:amd:array |
[C] | applyPluginsBailResult | evaluate Literal |
[C] | applyPluginsBailResult | call require:commonjs:item |
[C] | applyPluginsBailResult | statement |
[C] | applyPluginsBailResult | evaluate MemberExpression |
[C] | applyPluginsBailResult | evaluate Identifier console.log |
[C] | applyPluginsBailResult | call console.log |
[C] | applyPluginsBailResult | expression console.log |
[C] | applyPluginsBailResult | expression console |
[A] | applyPlugins | succeed-module |
[E] | applyPluginsAsyncWaterfall | before-resolve |
[B] | applyPluginsWaterfall | factory |
[A] | applyPlugins | build-module |
[A] | applyPlugins | succeed-module |
[A] | applyPlugins | seal |
[A] | applyPlugins | optimize |
[A] | applyPlugins | optimize-modules |
[A] | applyPlugins | after-optimize-modules |
[A] | applyPlugins | optimize-chunks |
[A] | applyPlugins | after-optimize-chunks |
[D] | applyPluginsAsyncSeries | optimize-tree |
[A] | applyPlugins | after-optimize-tree |
[C] | applyPluginsBailResult | should-record |
[A] | applyPlugins | revive-modules |
[A] | applyPlugins | optimize-module-order |
[A] | applyPlugins | before-module-ids |
[A] | applyPlugins | optimize-module-ids |
[A] | applyPlugins | after-optimize-module-ids |
[A] | applyPlugins | record-modules |
[A] | applyPlugins | revive-chunks |
[A] | applyPlugins | optimize-chunk-order |
[A] | applyPlugins | before-chunk-ids |
[A] | applyPlugins | optimize-chunk-ids |
[A] | applyPlugins | after-optimize-chunk-ids |
[A] | applyPlugins | record-chunks |
[A] | applyPlugins | before-hash |
[A] | applyPlugins | hash |
[A] | applyPlugins | hash |
[A] | applyPlugins | hash |
[A] | applyPlugins | hash |
[A] | applyPlugins | hash-for-chunk |
[A] | applyPlugins | chunk-hash |
[A] | applyPlugins | after-hash |
[A] | applyPlugins | before-chunk-assets |
[B] | applyPluginsWaterfall | global-hash-paths |
[C] | applyPluginsBailResult | global-hash |
[B] | applyPluginsWaterfall | bootstrap |
[B] | applyPluginsWaterfall | local-vars |
[B] | applyPluginsWaterfall | require |
[B] | applyPluginsWaterfall | module-obj |
[B] | applyPluginsWaterfall | module-require |
[B] | applyPluginsWaterfall | require-extensions |
[B] | applyPluginsWaterfall | asset-path |
[B] | applyPluginsWaterfall | startup |
[B] | applyPluginsWaterfall | module-require |
[B] | applyPluginsWaterfall | render |
[B] | applyPluginsWaterfall | module |
[B] | applyPluginsWaterfall | render |
[B] | applyPluginsWaterfall | package |
[B] | applyPluginsWaterfall | module |
[B] | applyPluginsWaterfall | render |
[B] | applyPluginsWaterfall | package |
[B] | applyPluginsWaterfall | modules |
[B] | applyPluginsWaterfall | render-with-entry |
[B] | applyPluginsWaterfall | asset-path |
[B] | applyPluginsWaterfall | asset-path |
[A] | applyPlugins | chunk-asset |
[A] | applyPlugins | additional-chunk-assets |
[A] | applyPlugins | record |
[D] | applyPluginsAsyncSeries | additional-assets |
[D] | applyPluginsAsyncSeries | optimize-chunk-assets |
[A] | applyPlugins | after-optimize-chunk-assets |
[D] | applyPluginsAsyncSeries | optimize-assets |
[A] | applyPlugins | after-optimize-assets |
[D] | applyPluginsAsyncSeries | after-compile |
[C] | applyPluginsBailResult | should-emit |
[D] | applyPluginsAsyncSeries | emit |
[B] | applyPluginsWaterfall | asset-path |
[D] | applyPluginsAsyncSeries | after-emit |
[A] | applyPlugins | done |
內容較多,依據源碼內容的編排,能夠將上述進行分層;大粒度的事件流以下:
而其中 make、 seal 和 emit 階段比較核心(包含了不少小粒度的事件),後續會繼續展開講解;
這裏羅列一下關鍵的事件節點:
entry-option
:初始化optionsrun
:開始編譯make
:從entry開始遞歸的分析依賴,對每一個依賴模塊進行buildbefore-resolve - after-resolve
: 對其中一個模塊位置進行解析build-module
:開始構建 (build) 這個module,這裏將使用文件對應的loader加載normal-module-loader
:對用loader加載完成的module(是一段js代碼)進行編譯,用 acorn 編譯,生成ast抽象語法樹。program
: 開始對ast進行遍歷,當遇到require等一些調用表達式時,觸發 call require
事件的handler執行,收集依賴,並。如:AMDRequireDependenciesBlockParserPlugin等seal
: 全部依賴build完成,下面將開始對chunk進行優化,好比合並,抽取公共模塊,加hashoptimize-chunk-assets
:壓縮代碼,插件 UglifyJsPlugin 就放在這個階段bootstrap
: 生成啓動代碼emit
: 把各個chunk輸出到結果文件本系列的源碼閱讀,如下幾篇文章給了不少啓發和思路,其中 webpack 源碼解析 和 細說 webpack 之流程篇 尤其突出,推薦閱讀;
下面的是個人公衆號二維碼圖片,歡迎關注。