Webpack 源碼(一)—— Tapable 和 事件流

一、Tapable

Tap 的英文單詞解釋,除了最經常使用的 點擊 手勢以外,還有一個意思是 水龍頭 —— 在 webpack 中指的是後一種;html

Webpack 能夠認爲是一種基於事件流的編程範例,內部的工做流程都是基於 插件 機制串接起來;webpack

而將這些插件粘合起來的就是webpack本身寫的基礎類 Tapable 是,plugin方法就是該類暴露出來的;git

後面咱們將看到核心的對象 Compiler、Compilation 等都是繼承於該對象

基於該類規範而其的 Webpack 體系保證了插件的有序性,使得整個系統很是有彈性,擴展性很好;然而有一個致命的缺點就是調試、看源碼真是很痛苦,各類跳來跳去;(基於事件流的寫法,和程序語言中的 goto 語句很相似)github

把這個倉庫下載,使用 Webstorm 進行調試,test 目錄是很好的教程入口;web

Tapable.plugin():至關於把對象歸類到名爲 name 的對象下,以array的形式;全部的插件都存在私有變量 _plugin 變量中;shell

plugins

接下來咱們簡單節選幾個函數分析一下:express

1.一、apply 方法

該方法最普通也是最經常使用的,看一下它的定義:編程

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 的上下文

1.二、 applyPluginsAsync(name,...other,callback)

// 模擬兩個插件
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,中斷後續插件的運行;

1.三、 applyPluginsParallel(name,...other,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

1.四、 applyPluginsWaterfall(name, init, callback)

顧名思義,這個方法至關因而 瀑布式 調用,給第一個插件傳入初始對象 init,而後通過第一個插件調用以後會得到一個結果對象,該結果對象會傳給下一個插件 做爲初始值,直到最後調用完畢,最後一個插件的直接結果傳給 callback 做爲初始值;

1.五、 applyPluginsParallelBailResult(name,...other,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 執行時機是和執行時間有關係的,你能夠本身驗證一下;

1.六、總結

總結一下,Tapable 就至關因而一個 事件管家,它所提供的 plugin 方法相似於 addEventListen 監聽事件,apply 方法相似於事件觸發函數 trigger

總結一下

二、Webpack 中的事件流

既然 Webpack 是基於 Tapable 搭建起來的,那麼咱們看一下 Webpack 構建一個模塊的基本事件流是如何的;

咱們在 Webpack 庫中的 Tapable.js 中每一個方法中新增 console 語句打出日誌,就能找出全部關鍵的事件名字:

log

打印結果:(這裏只列舉了簡單的事件流程,打包不一樣的入口文件會有所差別,但 事件出現的前後順序是固定的

類型 名字 事件名
[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

內容較多,依據源碼內容的編排,能夠將上述進行分層;大粒度的事件流以下:

大力度事件流

而其中 makesealemit 階段比較核心(包含了不少小粒度的事件),後續會繼續展開講解;

這裏羅列一下關鍵的事件節點:

  • entry-option:初始化options
  • run:開始編譯
  • make:從entry開始遞歸的分析依賴,對每一個依賴模塊進行build
  • before-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進行優化,好比合並,抽取公共模塊,加hash
  • optimize-chunk-assets:壓縮代碼,插件 UglifyJsPlugin 就放在這個階段
  • bootstrap: 生成啓動代碼
  • emit: 把各個chunk輸出到結果文件

三、參考文章

本系列的源碼閱讀,如下幾篇文章給了不少啓發和思路,其中 webpack 源碼解析細說 webpack 之流程篇 尤其突出,推薦閱讀;

下面的是個人公衆號二維碼圖片,歡迎關注。
我的微信公衆號

相關文章
相關標籤/搜索