最近在看webpack
的源碼,發現有個比較頭疼的點是:代碼看起來很是跳躍,每每看不到幾行就插入一段新內容,爲了理解又不得不先學習相關的前置知識。層層嵌套以後,發現最基礎的仍是tapable
模型,所以先對這部分的內容作一個介紹。前端
Webpack
的流程能夠分爲如下三大階段:node
初始化:啓動構建,讀取與合併配置參數,加載 Plugin
,實例化 Compiler。這個compile對象會穿行在本次編譯的整個週期。
編譯:從 Entry 發出,針對每一個 Module 串行調用對應的 Loader 去翻譯文件內容,再找到該 Module 依賴的 Module,遞歸地進行編譯處理。
輸出:對編譯後的 Module
組合成 Chunk
,把 Chunk
轉換成文件,輸出到文件系統。webpack
在這個過程當中,最核心的就是插件化的設計: 在不一樣的階段執行相應的一些插件,來執行某些功能。
而這裏的階段,指的就是hook
。 理論太抽象,來看一段webpack的源碼(4.x版本):web
// webpack/lib/MultiCompiler.js const { Tapable, SyncHook, MultiHook } = require("tapable"); class MultiCompiler extends Tapable { constructor(compilers) { super(); this.hooks = { done: new SyncHook(["stats"]), invalid: new MultiHook(compilers.map(c => c.hooks.invalid)), run: new MultiHook(compilers.map(c => c.hooks.run)), watchClose: new SyncHook([]), watchRun: new MultiHook(compilers.map(c => c.hooks.watchRun)), infrastructureLog: new MultiHook( compilers.map(c => c.hooks.infrastructureLog) ) }; } /// 省略其餘代碼 }
這是compile
的構造函數,有幾個注意點:數組
Tapable
,也就是本文的話題對象done
invalid
, run
, watchClose
等等都是內置的生命週期,具體的代碼暫時不去關心。這部分代碼主要是爲了說明一個思路: webpack 的生命週期hook
,其實是一個個插件的集合,表明的含義是,在某個階段須要掛載某些插件。
到這裏,腦海裏有這種大概雛形就好,接下來咱們開始介紹Tapable
。promise
Tapable的核心思路有點相似於nodejs
中的events
,最基本的發佈/訂閱模式。併發
const EventEmitter = require('events'); const myEmitter = new EventEmitter(); // 註冊事件對應的監聽函數 myEmitter.on('安歌發佈新文章', (title, tag) => { console.log("前去圍觀並吐槽",title, tag) }); // 觸發事件 並傳入參數 myEmitter.emit('安歌發佈新文章',’標題tapable機制‘, '標籤webpack');
這個結構很簡單也很清晰:異步
tapable
的核心用法與此類似,那爲何屢次一舉要使用它呢?async
根據前面的demo,不妨假設一下,若是咱們註冊了不少事件,好比event.on(’起牀‘)
,event.on(’吃飯‘)
,event.on(’上班‘)
等等,那事件之間可能就存在一些依賴關係,好比要先起牀而後才能上班這樣的時序依賴,而tapable
就能夠幫助咱們很方便的管理這些關係。函數
接下來用一個前幾天參加的公司中秋晚會的例子,來簡單說明一下Tapable
的用法:
我把本身的參加流程分紅如下階段:
晚宴前
晚宴中
晚宴後
那麼先寫個全局demo:
// 1. 引入 tapable ,先無論具體的鉤子類型 const { SyncHook, SyncBailHook, SyncWaterfallHook, } = require("tapable"); // 2. 定義不一樣階段對應的鉤子, // 鉤子: 晚宴前 let beforeDinner = new SyncHook(["stageName"]); // 鉤子:晚宴中 let atTheDinner = new SyncBailHook(["stageName"]); // 鉤子 晚宴後 let afterDinner = new SyncWaterfallHook(["stageName"]); // 3. 爲不一樣階段註冊事件,這裏先寫出晚宴前的事件 beforeDinner.tap('檢查着裝', (stageName)=>{ console.log(`${stageName}: 檢查着裝`) }) beforeDinner.tap('乘坐班車到酒店', (stageName)=>{ console.log(`${stageName}: 乘坐班車到酒店`) }) // 每一個階段觸發自身須要執行的事件 beforeDinner.call('晚宴前'); atTheDinner.call('晚宴中'); afterDinner.call('晚宴後'); // 輸出結果: // 晚宴前: 檢查着裝 // 晚宴前: 乘坐班車到酒店 // ... 省略後面的輸出
這個demo
簡單的定義了三個階段,先不去關具體的hook
類型,瞭解下總體的結構:
beforeDinner
在實例化時,使用數組聲明瞭參數stageName
, 這個地方的參數類型僅僅做爲接口定義的目的使用,爲了方便觸發的時候傳入對應的參數;call
方法其實就相似前文的emit
,與之不一樣的是,event.emit
表示事件觸發,而hook.call
表示當前鉤子要開始執行鉤子上註冊的全部事件。(固然咱們只註冊了晚宴前的2個事件)hook.call(param)
執行以後,該hook
對應的事件就按照註冊順序以及特定規則(具體規則後面說明,暫時略過)依次執行,所以上面的beforeDinner.call('晚宴前');
會輸出對應的階段名稱和事件名稱。到這裏,咱們已經用上了最基本的tapable
了。回顧下它和events
最大的區別:
tapable
不只提供了事件的註冊和執行,還用不一樣的Hook
將事件進行分類(這裏例子用三個階段將基礎事件分類)
SyncBailHook
接下來就是晚宴中的事件,這裏有個注意點:晚宴中的第三個事件」若是成爲當前桌狀元,那麼就留下來博王中王「是一個帶有前提條件的事件,因此咱們用了SyncBailHook
,而且這麼註冊事件:
atTheDinner.tap('用餐並欣賞表演', (stageName) => { console.log(`${stageName}: 用餐並欣賞表演`); }) atTheDinner.tap('在當前桌進行博餅', (stageName) => { console.log(`${stageName}: 在當前桌進行博餅`); // 關鍵僞代碼 let getChampion = false //若是得到狀元 if(!getChampion){ console.log(`${stageName}: 沒有得到當前桌狀元,不須要參與博王中王`); // 注意這裏的return return '提早結束!'; } }) atTheDinner.tap('博王中王', (stageName) => { console.log(`${stageName}: 博王中王`); })
SyncBailHook翻譯過來意思是「熔斷類型的鉤子」,做用就像保險絲,一旦有危險,則啓動保護(一旦該鉤子的某個事件,執行返回除了undefined之外的值,後面註冊的事件就再也不執行)。正如前面的例子中,若是在「當前桌子博餅」中沒有成功搏到「狀元」,就不會進行後面的「搏王中王」事件。經常使用於處理某些須要條件判斷才觸發的事件。
SyncWaterfallHook
晚宴以後的事件,與前面不用的地方在於:事件2發朋友圈 用的是事件1中所拍的照片,換句話說後面的事件依賴於前面事件的執行結果。因此能夠這麼寫:
afterDinner.tap('回家前拍照', (stageName) => { console.log(`${stageName}: 拍一些照片,打車回家`); let pictures = ['image1','image2']; return pictures; }) afterDinner.tap('回家後發朋友圈', (pictures)=> { // 注意這裏的內置參數 再也不是stageName 而是pictures return console.log(`回家後,用${pictures}:發朋友圈`); })
實例化afterDinner
時使用了SyncWaterfallHook
,顧名思義,這種瀑布式的鉤子,做用就是在執行該鉤子內註冊的事件時,會把每一個階段的執行結果傳遞給後面的階段。
這部分咱們介紹了tapable
的基本用法和三種基本類型的hook
,大概能夠總結一下:
hook
表示事件的集合,hook
的類型決定了註冊在這個hook
的事件如何執行
hook
開胃菜結束,接下來要真正開始系統化的瞭解tapable
了,(好消息是若是前面的例子都看懂了,後面的學起來會很是簡單,壞消息是:又要涉及前端最棘手的問題之一--異步)
先來一覽全部的hook
類型:
整體上,hook
類型分紅同步和異步兩大類,異步再分爲異步串行和異步並行。
先前已經介紹了同步hook
裏面的前三種。第四種SynloopHook
也簡要介紹下:
假設寫文章這個事情,分紅校對和發表兩個步驟,校對必須3次以上,才能夠執行發表事件:
// 當監聽函數被觸發的時候,若是該監聽函數返回true時則這個監聽函數會反覆執行,若是返回 undefined 則表示退出循環 let writeArticle = SyncLoopHook(); let count = 0; writeArticle.tap('校對',()=>{ console.log('執行校對', count++) if(count<3){ return true; // 沒有達到3次則繼續校對 } return }) writeArticle.tap('發表',()=>{ console.log('發表') })
AsyncParallelHook
異步的hook,註冊和觸發能夠用tapAsync/callAsync
和tapPromise/promise
兩種語法,寫法上略有不用。直接上demo:
// AsyncParallelHook 鉤子:tapAsync/callAsync 的使用 const { AsyncParallelHook } = require("tapable"); // 建立實例 let asyncParallelHook = new AsyncParallelHook(["demoName"]); // 註冊事件 console.time("time"); asyncParallelHook.tapAsync("異步事件1", (demoName, done) => { setTimeout(() => { console.log("1", demoName, new Date()); done(); //須要注意的是這裏的`done`方法 }, 1000); }); asyncParallelHook.tapAsync("異步事件2", (demoName, done) => { setTimeout(() => { console.log("2", demoName, new Date()); done(); }, 2000); }); asyncParallelHook.tapAsync("異步事件3", (demoName, done) => { setTimeout(() => { console.log("3", demoName, new Date()); done(); console.timeEnd("time"); }, 3000); }); // 觸發事件,讓監聽函數執行 asyncParallelHook.callAsync("異步並行", () => { // 只有當前鉤子的全部事件都執行done 才進入這個callback console.log("complete"); }); // 輸出 // 異步事件1 異步並行 // Sun Sep 08 2019 21:24:12 GMT+0800 (GMT+08:00) {} // 異步事件2 異步並行 // Sun Sep 08 2019 21:24:13 GMT+0800 (GMT+08:00) {} // 異步事件3 異步並行 // Sun Sep 08 2019 21:24:14 GMT+0800 (GMT+08:00) {} // complete // time: 3007.266845703125ms // time: 3007.640ms
須要注意的是這裏的done
方法, 每一個註冊的的事件均可以調用到這個done
方法,這個方法的做用是:向對應的hook實例告知,當前的異步事件完成,只有當全部的事件回調都執行了done
方法,纔會進入鉤子自己的回調函數(demo中的console.log("complete");)
從例子中的計時狀況來看,很明顯全部的事件是並行的 -- 事件1 2 3分別須要1s 2s 3s, 最終執行完也只花了3s。
用tapPromise/promise
來寫的話,以下:
asyncParallelHook.tapPromise("異步事件1", (demoName) => { return new Promise((resolve, reject) => { setTimeout(() => { console.log("1", demoName, new Date()); resolve("1"); }, 1000); }); }); // ...省略重複代碼 asyncParallelHook.promise("異步並行").then(() => { console.log("最終結果", new Date()); }).catch(err => { console.log("發現錯誤", new Date()); });
區別在於:
tapPromise
註冊時,回調函數必須返回一個promise
tabAsync
註冊使用done
表示當前執行完成,使用tapPromise
時則只要使用resolve()
便可resolve
, 而是reject(error)
,那麼會進入asyncParallelHook
的catch
而不是then
這種寫法其實很相似ES6中的promise.all
,比較好理解
AsyncSeriesHook
其實到這裏,已經一隻腳踏進成功的大門了。 異步串行和異步並行的寫法,徹底同樣。只須要簡單把前面例子中,實例化的語句改爲:
let asyncSeriesHook = new AsyncSeriesHook()
而後看看3個異步事件執行完後的事件間隔(並行的時候是3s,串行時總時長變成6s)。
沒錯,就是這麼簡單~!
webpack-dev-middleware
中tapable
的應用webpack-dev-middleware
是一個webpack的插件,做用是監聽webpack的編譯變化並寫入到內存中。 核心代碼:
// webpack-dev-middleware/lib/context.js const context = { state: false, webpackStats: null, // callbacks: [], options, compiler, watching: null, forceRebuild: false, }; function invalid(callback) { if (context.state) { context.options.reporter(context.options, { log, state: false, }); } // We are now in invalid state context.state = false; if (typeof callback === 'function') { callback(); } } // 關鍵代碼 利用compile的hook 觀察編譯變化 並插入操做 context.compiler.hooks.invalid.tap('WebpackDevMiddleware', invalid); context.compiler.hooks.run.tap('WebpackDevMiddleware', invalid); context.compiler.hooks.done.tap('WebpackDevMiddleware', done); context.compiler.hooks.watchRun.tap( 'WebpackDevMiddleware', (comp, callback) => { invalid(callback); } );
核心的代碼就是使用webpack
提供的內置hook
watchRun
來插入自定義的操做(檢查編譯狀況,生成臨時結果到內存)
呼~ tapable的內容大概寫完了,本文介紹了同步的幾種鉤子,和異步的2種表明性的鉤子,至於異步並行熔斷等等,就是前面介紹的鉤子的合成,比較簡單。回顧一下主要的內容:
tapAsync/callAsync
和tapPromise/promise
兩種使用方式,而且均可以觀察事件總體執行結果;理解清楚tapable以後,再開始學習webpack的源碼,會相對順暢一些。
-----慣例偷懶分割線-----
若是以爲寫得很差/有錯誤/表述不明確,都歡迎指出
若是有幫助,歡迎點贊和收藏,轉載請徵得贊成後著明出處。若是有問題也歡迎私信交流,主頁有郵箱地址
若是以爲做者很辛苦,也歡迎打賞~