本文介紹了Webpack核心模塊tapable,分享給大家,具體如下:
前言
Webpack 是一個(gè)現(xiàn)代 JavaScript 應(yīng)用程序的靜態(tài)模塊打包器,是對(duì)前端項(xiàng)目實(shí)現(xiàn)自動(dòng)化和優(yōu)化必不可少的工具,Webpack 的 loader (加載器)和 plugin (插件)是由 Webpack 開發(fā)者和社區(qū)開發(fā)者共同貢獻(xiàn)的,而目前又沒有比較系統(tǒng)的開發(fā)文檔,想寫加載器和插件必須要懂 Webpack 的原理,即看懂 Webpack 的源碼, tapable 則是 Webpack 依賴的核心庫,可以說不懂 tapable 就看不懂 Webpack 源碼,所以本篇會(huì)對(duì) tapable 提供的類進(jìn)行解析和模擬。
tapable 介紹
Webpack 本質(zhì)上是一種事件流的機(jī)制,它的工作流程就是將各個(gè)插件串聯(lián)起來,而實(shí)現(xiàn)這一切的核心就是 tapable ,Webpack 中最核心的,負(fù)責(zé)編譯的 Compiler 和負(fù)責(zé)創(chuàng)建 bundles 的 Compilation 都是 tapable 構(gòu)造函數(shù)的實(shí)例。
打開 Webpack 4.0 的源碼中一定會(huì)看到下面這些以 Sync 、 Async 開頭,以 Hook 結(jié)尾的方法,這些都是 tapable 核心庫的類,為我們提供不同的事件流執(zhí)行機(jī)制,我們稱為 “鉤子”。
// 引入 tapable 如下 const { SyncHook, SyncBailHook, SyncWaterfallHook, SyncLoopHook, AsyncParallelHook, AsyncParallelBailHook, AsyncSeriesHook, AsyncSeriesBailHook, AsyncSeriesWaterfallHook } = require("tapable");
上面的實(shí)現(xiàn)事件流機(jī)制的 “鉤子” 大方向可以分為兩個(gè)類別,“同步” 和 “異步”,“異步” 又分為兩個(gè)類別,“并行” 和 “串行”,而 “同步” 的鉤子都是串行的。
Sync 類型的鉤子
1、SyncHook
SyncHook 為串行同步執(zhí)行,不關(guān)心事件處理函數(shù)的返回值,在觸發(fā)事件之后,會(huì)按照事件注冊的先后順序執(zhí)行所有的事件處理函數(shù)。
// SyncHook 鉤子的使用 const { SyncHook } = require("tapable"); // 創(chuàng)建實(shí)例 let syncHook = new SyncHook(["name", "age"]); // 注冊事件 syncHook.tap("1", (name, age) => console.log("1", name, age)); syncHook.tap("2", (name, age) => console.log("2", name, age)); syncHook.tap("3", (name, age) => console.log("3", name, age)); // 觸發(fā)事件,讓監(jiān)聽函數(shù)執(zhí)行 syncHook.call("panda", 18); // 1 panda 18 // 2 panda 18 // 3 panda 18
在 tapable 解構(gòu)的 SyncHook 是一個(gè)類,注冊事件需先創(chuàng)建實(shí)例,創(chuàng)建實(shí)例時(shí)支持傳入一個(gè)數(shù)組,數(shù)組內(nèi)存儲(chǔ)事件觸發(fā)時(shí)傳入的參數(shù),實(shí)例的 tap 方法用于注冊事件,支持傳入兩個(gè)參數(shù),第一個(gè)參數(shù)為事件名稱,在 Webpack 中一般用于存儲(chǔ)事件對(duì)應(yīng)的插件名稱(名字隨意,只是起到注釋作用), 第二個(gè)參數(shù)為事件處理函數(shù),函數(shù)參數(shù)為執(zhí)行 call 方法觸發(fā)事件時(shí)所傳入的參數(shù)的形參。
// 模擬 SyncHook 類 class SyncHook { constructor(args) { this.args = args; this.tasks = []; } tap(name, task) { this.tasks.push(task); } call(...args) { // 也可在參數(shù)不足時(shí)拋出異常 if (args.length < this.args.length) throw new Error("參數(shù)不足"); // 傳入?yún)?shù)嚴(yán)格對(duì)應(yīng)創(chuàng)建實(shí)例傳入數(shù)組中的規(guī)定的參數(shù),執(zhí)行時(shí)多余的參數(shù)為 undefined args = args.slice(0, this.args.length); // 依次執(zhí)行事件處理函數(shù) this.tasks.forEach(task => task(...args)); } }
tasks 數(shù)組用于存儲(chǔ)事件處理函數(shù), call 方法調(diào)用時(shí)傳入?yún)?shù)超過創(chuàng)建 SyncHook 實(shí)例傳入的數(shù)組長度時(shí),多余參數(shù)可處理為 undefined ,也可在參數(shù)不足時(shí)拋出異常,不靈活,后面的例子中就不再這樣寫了。
2、SyncBailHook
SyncBailHook 同樣為串行同步執(zhí)行,如果事件處理函數(shù)執(zhí)行時(shí)有一個(gè)返回值不為空(即返回值為 undefined ),則跳過剩下未執(zhí)行的事件處理函數(shù)(如類的名字,意義在于保險(xiǎn))。
// SyncBailHook 鉤子的使用 const { SyncBailHook } = require("tapable"); // 創(chuàng)建實(shí)例 let syncBailHook = new SyncBailHook(["name", "age"]); // 注冊事件 syncBailHook.tap("1", (name, age) => console.log("1", name, age)); syncBailHook.tap("2", (name, age) => { console.log("2", name, age); return "2"; }); syncBailHook.tap("3", (name, age) => console.log("3", name, age)); // 觸發(fā)事件,讓監(jiān)聽函數(shù)執(zhí)行 syncBailHook.call("panda", 18); // 1 panda 18 // 2 panda 18
通過上面的用法可以看出, SyncHook 和 SyncBailHook 在邏輯上只是 call 方法不同,導(dǎo)致事件的執(zhí)行機(jī)制不同,對(duì)于后面其他的 “鉤子”,也是 call 的區(qū)別,接下來實(shí)現(xiàn) SyncBailHook 類。
// 模擬 SyncBailHook 類 class SyncBailHook { constructor(args) { this.args = args; this.tasks = []; } tap(name, task) { this.tasks.push(task); } call(...args) { // 傳入?yún)?shù)嚴(yán)格對(duì)應(yīng)創(chuàng)建實(shí)例傳入數(shù)組中的規(guī)定的參數(shù),執(zhí)行時(shí)多余的參數(shù)為 undefined args = args.slice(0, this.args.length); // 依次執(zhí)行事件處理函數(shù),如果返回值不為空,則停止向下執(zhí)行 let i = 0, ret; do { ret = this.tasks[i++](...args); } while (!ret); } }
在上面代碼的 call 方法中,我們設(shè)置返回值為 ret ,第一次執(zhí)行后沒有返回值則繼續(xù)循環(huán)執(zhí)行,如果有返回值則立即停止循環(huán),即實(shí)現(xiàn) “保險(xiǎn)” 的功能。
3、SyncWaterfallHook
SyncWaterfallHook 為串行同步執(zhí)行,上一個(gè)事件處理函數(shù)的返回值作為參數(shù)傳遞給下一個(gè)事件處理函數(shù),依次類推,正因如此,只有第一個(gè)事件處理函數(shù)的參數(shù)可以通過 call 傳遞,而 call 的返回值為最后一個(gè)事件處理函數(shù)的返回值。
// SyncWaterfallHook 鉤子的使用 const { SyncWaterfallHook } = require("tapable"); // 創(chuàng)建實(shí)例 let syncWaterfallHook = new SyncWaterfallHook(["name", "age"]); // 注冊事件 syncWaterfallHook.tap("1", (name, age) => { console.log("1", name, age); return "1"; }); syncWaterfallHook.tap("2", data => { console.log("2", data); return "2"; }); syncWaterfallHook.tap("3", data => { console.log("3", data); return "3" }); // 觸發(fā)事件,讓監(jiān)聽函數(shù)執(zhí)行 let ret = syncWaterfallHook.call("panda", 18); console.log("call", ret); // 1 panda 18 // 2 1 // 3 2 // call 3
SyncWaterfallHook 名稱中含有 “瀑布”,通過上面代碼可以看出 “瀑布” 形象生動(dòng)的描繪了事件處理函數(shù)執(zhí)行的特點(diǎn),與 SyncHook 和 SyncBailHook 的區(qū)別就在于事件處理函數(shù)返回結(jié)果的流動(dòng)性,接下來看一下 SyncWaterfallHook 類的實(shí)現(xiàn)。
// 模擬 SyncWaterfallHook 類 class SyncWaterfallHook { constructor(args) { this.args = args; this.tasks = []; } tap(name, task) { this.tasks.push(task); } call(...args) { // 傳入?yún)?shù)嚴(yán)格對(duì)應(yīng)創(chuàng)建實(shí)例傳入數(shù)組中的規(guī)定的參數(shù),執(zhí)行時(shí)多余的參數(shù)為 undefined args = args.slice(0, this.args.length); // 依次執(zhí)行事件處理函數(shù),事件處理函數(shù)的返回值作為下一個(gè)事件處理函數(shù)的參數(shù) let [first, ...others] = this.tasks; return reduce((ret, task) => task(ret), first(...args)); } }
上面代碼中 call 的邏輯是將存儲(chǔ)事件處理函數(shù)的 tasks 拆成兩部分,分別為第一個(gè)事件處理函數(shù),和存儲(chǔ)其余事件處理函數(shù)的數(shù)組,使用 reduce 進(jìn)行歸并,將第一個(gè)事件處理函數(shù)執(zhí)行后的返回值作為歸并的初始值,依次調(diào)用其余事件處理函數(shù)并傳遞上一次歸并的返回值。
4、SyncLoopHook
SyncLoopHook 為串行同步執(zhí)行,事件處理函數(shù)返回 true 表示繼續(xù)循環(huán),即循環(huán)執(zhí)行當(dāng)前事件處理函數(shù),返回 undefined 表示結(jié)束循環(huán), SyncLoopHook 與 SyncBailHook 的循環(huán)不同, SyncBailHook 只決定是否繼續(xù)向下執(zhí)行后面的事件處理函數(shù),而 SyncLoopHook 的循環(huán)是指循環(huán)執(zhí)行每一個(gè)事件處理函數(shù),直到返回 undefined 為止,才會(huì)繼續(xù)向下執(zhí)行其他事件處理函數(shù),執(zhí)行機(jī)制同理。
// SyncLoopHook 鉤子的使用 const { SyncLoopHook } = require("tapable"); // 創(chuàng)建實(shí)例 let syncLoopHook = new SyncLoopHook(["name", "age"]); // 定義輔助變量 let total1 = 0; let total2 = 0; // 注冊事件 syncLoopHook.tap("1", (name, age) => { console.log("1", name, age, total1); return total1++ < 2 ? true : undefined; }); syncLoopHook.tap("2", (name, age) => { console.log("2", name, age, total2); return total2++ < 2 ? true : undefined; }); syncLoopHook.tap("3", (name, age) => console.log("3", name, age)); // 觸發(fā)事件,讓監(jiān)聽函數(shù)執(zhí)行 syncLoopHook.call("panda", 18); // 1 panda 18 0 // 1 panda 18 1 // 1 panda 18 2 // 2 panda 18 0 // 2 panda 18 1 // 2 panda 18 2 // 3 panda 18
通過上面的執(zhí)行結(jié)果可以清楚的看到 SyncLoopHook 的執(zhí)行機(jī)制,但有一點(diǎn)需要注意,返回值必須嚴(yán)格是 true 才會(huì)觸發(fā)循環(huán),多次執(zhí)行當(dāng)前事件處理函數(shù),必須嚴(yán)格返回 undefined ,才會(huì)結(jié)束循環(huán),去執(zhí)行后面的事件處理函數(shù),如果事件處理函數(shù)的返回值不是 true 也不是 undefined ,則會(huì)死循環(huán)。
在了解 SyncLoopHook 的執(zhí)行機(jī)制以后,我們接下來看看 SyncLoopHook 的 call 方法是如何實(shí)現(xiàn)的。
// 模擬 SyncLoopHook 類 class SyncLoopHook { constructor(args) { this.args = args; this.tasks = []; } tap(name, task) { this.tasks.push(task); } call(...args) { // 傳入?yún)?shù)嚴(yán)格對(duì)應(yīng)創(chuàng)建實(shí)例傳入數(shù)組中的規(guī)定的參數(shù),執(zhí)行時(shí)多余的參數(shù)為 undefined args = args.slice(0, this.args.length); // 依次執(zhí)行事件處理函數(shù),如果返回值為 true,則繼續(xù)執(zhí)行當(dāng)前事件處理函數(shù) // 直到返回 undefined,則繼續(xù)向下執(zhí)行其他事件處理函數(shù) this.tasks.forEach(task => { let ret; do { ret = this.task(...args); } while (ret === true || !(ret === undefined)); }); } }
在上面代碼中可以看到 SyncLoopHook 類 call 方法的實(shí)現(xiàn)更像是 SyncHook 和 SyncBailHook 的 call 方法的結(jié)合版,外層循環(huán)整個(gè) tasks 事件處理函數(shù)隊(duì)列,內(nèi)層通過返回值進(jìn)行循環(huán),控制每一個(gè)事件處理函數(shù)的執(zhí)行次數(shù)。
注意:在 Sync 類型 “鉤子” 下執(zhí)行的插件都是順序執(zhí)行的,只能使用 tab 注冊。
Async 類型的鉤子
Async 類型可以使用 tap 、 tapSync 和 tapPromise 注冊不同類型的插件 “鉤子”,分別通過 call 、 callAsync 和 promise 方法調(diào)用,我們下面會(huì)針對(duì) AsyncParallelHook 和 AsyncSeriesHook 的 async 和 promise 兩種方式分別介紹和模擬。
1、AsyncParallelHook
AsyncParallelHook 為異步并行執(zhí)行,通過 tapAsync 注冊的事件,通過 callAsync 觸發(fā),通過 tapPromise 注冊的事件,通過 promise 觸發(fā)(返回值可以調(diào)用 then 方法)。
(1) tapAsync/callAsync
callAsync 的最后一個(gè)參數(shù)為回調(diào)函數(shù),在所有事件處理函數(shù)執(zhí)行完畢后執(zhí)行。
// AsyncParallelHook 鉤子:tapAsync/callAsync 的使用 const { AsyncParallelHook } = require("tapable"); // 創(chuàng)建實(shí)例 let asyncParallelHook = new AsyncParallelHook(["name", "age"]); // 注冊事件 console.time("time"); asyncParallelHook.tapAsync("1", (name, age, done) => { settimeout(() => { console.log("1", name, age, new Date()); done(); }, 1000); }); asyncParallelHook.tapAsync("2", (name, age, done) => { settimeout(() => { console.log("2", name, age, new Date()); done(); }, 2000); }); asyncParallelHook.tapAsync("3", (name, age, done) => { settimeout(() => { console.log("3", name, age, new Date()); done(); console.timeEnd("time"); }, 3000); }); // 觸發(fā)事件,讓監(jiān)聽函數(shù)執(zhí)行 asyncParallelHook.callAsync("panda", 18, () => { console.log("complete"); }); // 1 panda 18 2018-08-07T10:38:32.675Z // 2 panda 18 2018-08-07T10:38:33.674Z // 3 panda 18 2018-08-07T10:38:34.674Z // complete // time: 3005.060ms
異步并行是指,事件處理函數(shù)內(nèi)三個(gè)定時(shí)器的異步操作最長時(shí)間為 3s ,而三個(gè)事件處理函數(shù)執(zhí)行完成總共用時(shí)接近 3s ,所以三個(gè)事件處理函數(shù)是幾乎同時(shí)執(zhí)行的,不需等待。
所有 tabAsync 注冊的事件處理函數(shù)最后一個(gè)參數(shù)都為一個(gè)回調(diào)函數(shù) done ,每個(gè)事件處理函數(shù)在異步代碼執(zhí)行完畢后調(diào)用 done 函數(shù),則可以保證 callAsync 會(huì)在所有異步函數(shù)都執(zhí)行完畢后執(zhí)行,接下來看一看 callAsync 是如何實(shí)現(xiàn)的。
// 模擬 AsyncParallelHook 類:tapAsync/callAsync class AsyncParallelHook { constructor(args) { this.args = args; this.tasks = []; } tabAsync(name, task) { this.tasks.push(task); } callAsync(...args) { // 先取出最后傳入的回調(diào)函數(shù) let finalCallback = args.pop(); // 傳入?yún)?shù)嚴(yán)格對(duì)應(yīng)創(chuàng)建實(shí)例傳入數(shù)組中的規(guī)定的參數(shù),執(zhí)行時(shí)多余的參數(shù)為 undefined args = args.slice(0, this.args.length); // 定義一個(gè) i 變量和 done 函數(shù),每次執(zhí)行檢測 i 值和隊(duì)列長度,決定是否執(zhí)行 callAsync 的回調(diào)函數(shù) let i = 0; let done = () => { if (++i === this.tasks.length) { finalCallback(); } }; // 依次執(zhí)行事件處理函數(shù) this.tasks.forEach(task => task(...args, done)); } }
在 callAsync 中,將最后一個(gè)參數(shù)(所有事件處理函數(shù)執(zhí)行完畢后執(zhí)行的回調(diào))取出,并定義 done 函數(shù),通過比較 i 和存儲(chǔ)事件處理函數(shù)的數(shù)組 tasks 的 length 來確定回調(diào)是否執(zhí)行,循環(huán)執(zhí)行每一個(gè)事件處理函數(shù)并將 done 作為最后一個(gè)參數(shù)傳入,所以每個(gè)事件處理函數(shù)內(nèi)部的異步操作完成時(shí),執(zhí)行 done 就是為了檢測是不是該執(zhí)行 callAsync 的回調(diào),當(dāng)所有事件處理函數(shù)均執(zhí)行完畢滿足 done 函數(shù)內(nèi)部 i 和 length 相等的條件時(shí),則調(diào)用 callAsync 的回調(diào)。
(2) tapPromise/promise
要使用 tapPromise 注冊事件,對(duì)事件處理函數(shù)有一個(gè)要求,必須返回一個(gè) Promise 實(shí)例,而 promise 方法也返回一個(gè) Promise 實(shí)例, callAsync 的回調(diào)函數(shù)在 promise 方法中用 then 的方式代替。
// AsyncParallelHook 鉤子:tapPromise/promise 的使用 const { AsyncParallelHook } = require("tapable"); // 創(chuàng)建實(shí)例 let asyncParallelHook = new AsyncParallelHook(["name", "age"]); // 注冊事件 console.time("time"); asyncParallelHook.tapPromise("1", (name, age) => { return new Promise((resolve, reject) => { settimeout(() => { console.log("1", name, age, new Date()); resolve("1"); }, 1000); }); }); asyncParallelHook.tapPromise("2", (name, age) => { return new Promise((resolve, reject) => { settimeout(() => { console.log("2", name, age, new Date()); resolve("2"); }, 2000); }); }); asyncParallelHook.tapPromise("3", (name, age) => { return new Promise((resolve, reject) => { settimeout(() => { console.log("3", name, age, new Date()); resolve("3"); console.timeEnd("time"); }, 3000); }); }); // 觸發(fā)事件,讓監(jiān)聽函數(shù)執(zhí)行 asyncParallelHook.promise("panda", 18).then(ret => { console.log(ret); }); // 1 panda 18 2018-08-07T12:17:21.741Z // 2 panda 18 2018-08-07T12:17:22.736Z // 3 panda 18 2018-08-07T12:17:23.739Z // time: 3006.542ms // [ '1', '2', '3' ]
上面每一個(gè) tapPromise 注冊事件的事件處理函數(shù)都返回一個(gè) Promise 實(shí)例,并將返回值傳入 resolve 方法,調(diào)用 promise 方法觸發(fā)事件時(shí),如果所有事件處理函數(shù)返回的 Promise 實(shí)例結(jié)果都成功,會(huì)將結(jié)果存儲(chǔ)在數(shù)組中,并作為參數(shù)傳遞給 promise 的 then 方法中成功的回調(diào),如果有一個(gè)失敗就是將失敗的結(jié)果返回作為參數(shù)傳遞給失敗的回調(diào)。
// 模擬 AsyncParallelHook 類 tapPromise/promise class AsyncParallelHook { constructor(args) { this.args = args; this.tasks = []; } tapPromise(name, task) { this.tasks.push(task); } promise(...args) { // 傳入?yún)?shù)嚴(yán)格對(duì)應(yīng)創(chuàng)建實(shí)例傳入數(shù)組中的規(guī)定的參數(shù),執(zhí)行時(shí)多余的參數(shù)為 undefined args = args.slice(0, this.args.length); // 將所有事件處理函數(shù)轉(zhuǎn)換成 Promise 實(shí)例,并發(fā)執(zhí)行所有的 Promise return Promise.all(this.tasks.map(task => task(...args))); } }
其實(shí)根據(jù)上面對(duì)于 tapPromise 和 promise 使用的描述就可以猜到, promise 方法的邏輯是通過 Promise.all 來實(shí)現(xiàn)的。
2、AsyncSeriesHook
AsyncSeriesHook 為異步串行執(zhí)行,與 AsyncParallelHook 相同,通過 tapAsync 注冊的事件,通過 callAsync 觸發(fā),通過 tapPromise 注冊的事件,通過 promise 觸發(fā),可以調(diào)用 then 方法。
(1) tapAsync/callAsync
與 AsyncParallelHook 的 callAsync 方法類似, AsyncSeriesHook 的 callAsync 方法也是通過傳入回調(diào)函數(shù)的方式,在所有事件處理函數(shù)執(zhí)行完畢后執(zhí)行 callAsync 的回調(diào)函數(shù)。
// AsyncSeriesHook 鉤子:tapAsync/callAsync 的使用 const { AsyncSeriesHook } = require("tapable"); // 創(chuàng)建實(shí)例 let asyncSeriesHook = new AsyncSeriesHook(["name", "age"]); // 注冊事件 console.time("time"); asyncSeriesHook.tapAsync("1", (name, age, next) => { settimeout(() => { console.log("1", name, age, new Date()); next(); }, 1000); }); asyncSeriesHook.tapAsync("2", (name, age, next) => { settimeout(() => { console.log("2", name, age, new Date()); next(); }, 2000); }); asyncSeriesHook.tapAsync("3", (name, age, next) => { settimeout(() => { console.log("3", name, age, new Date()); next(); console.timeEnd("time"); }, 3000); }); // 觸發(fā)事件,讓監(jiān)聽函數(shù)執(zhí)行 asyncSeriesHook.callAsync("panda", 18, () => { console.log("complete"); }); // 1 panda 18 2018-08-07T14:40:52.896Z // 2 panda 18 2018-08-07T14:40:54.901Z // 3 panda 18 2018-08-07T14:40:57.901Z // complete // time: 6008.790ms
異步串行是指,事件處理函數(shù)內(nèi)三個(gè)定時(shí)器的異步執(zhí)行時(shí)間分別為 1s 、 2s 和 3s ,而三個(gè)事件處理函數(shù)執(zhí)行完總共用時(shí)接近 6s ,所以三個(gè)事件處理函數(shù)執(zhí)行是需要排隊(duì)的,必須一個(gè)一個(gè)執(zhí)行,當(dāng)前事件處理函數(shù)執(zhí)行完才能執(zhí)行下一個(gè)。
AsyncSeriesHook 類的 tabAsync 方法注冊的事件處理函數(shù)參數(shù)中的 next 可以與 AsyncParallelHook 類中 tabAsync 方法參數(shù)的 done 進(jìn)行類比,同為回調(diào)函數(shù),不同點(diǎn)在于 AsyncSeriesHook 與 AsyncParallelHook 的 callAsync 方法的 “并行” 和 “串行” 的實(shí)現(xiàn)方式。
// 模擬 AsyncSeriesHook 類:tapAsync/callAsync class AsyncSeriesHook { constructor(args) { this.args = args; this.tasks = []; } tabAsync(name, task) { this.tasks.push(task); } callAsync(...args) { // 先取出最后傳入的回調(diào)函數(shù) let finalCallback = args.pop(); // 傳入?yún)?shù)嚴(yán)格對(duì)應(yīng)創(chuàng)建實(shí)例傳入數(shù)組中的規(guī)定的參數(shù),執(zhí)行時(shí)多余的參數(shù)為 undefined args = args.slice(0, this.args.length); // 定義一個(gè) i 變量和 next 函數(shù),每次取出一個(gè)事件處理函數(shù)執(zhí)行,并維護(hù) i 的值 // 直到所有事件處理函數(shù)都執(zhí)行完,調(diào)用 callAsync 的回調(diào) // 如果事件處理函數(shù)中沒有調(diào)用 next,則無法繼續(xù) let i = 0; let next = () => { let task = this.tasks[i++]; task ? task(...args, next) : finalCallback(); }; next(); } }
AsyncParallelHook 是通過循環(huán)依次執(zhí)行了所有的事件處理函數(shù), done 方法只為了檢測是否已經(jīng)滿足條件執(zhí)行 callAsync 的回調(diào),如果中間某個(gè)事件處理函數(shù)沒有調(diào)用 done ,只是不會(huì)調(diào)用 callAsync 的回調(diào),但是所有的事件處理函數(shù)都執(zhí)行了。
而 AsyncSeriesHook 的 next 執(zhí)行機(jī)制更像 Express 和 Koa 中的中間件,在注冊事件的回調(diào)中如果不調(diào)用 next ,則在觸發(fā)事件時(shí)會(huì)在沒有調(diào)用 next 的事件處理函數(shù)的位置 “卡死”,即不會(huì)繼續(xù)執(zhí)行后面的事件處理函數(shù),只有都調(diào)用 next 才能繼續(xù),而最后一個(gè)事件處理函數(shù)中調(diào)用 next 決定是否調(diào)用 callAsync 的回調(diào)。
(2) tapPromise/promise
與 AsyncParallelHook 類似, tapPromise 注冊事件的事件處理函數(shù)需要返回一個(gè) Promise 實(shí)例, promise 方法最后也返回一個(gè) Promise 實(shí)例。
// AsyncSeriesHook 鉤子:tapPromise/promise 的使用 const { AsyncSeriesHook } = require("tapable"); // 創(chuàng)建實(shí)例 let asyncSeriesHook = new AsyncSeriesHook(["name", "age"]); // 注冊事件 console.time("time"); asyncSeriesHook.tapPromise("1", (name, age) => { return new Promise((resolve, reject) => { settimeout(() => { console.log("1", name, age, new Date()); resolve("1"); }, 1000); }); }); asyncSeriesHook.tapPromise("2", (name, age) => { return new Promise((resolve, reject) => { settimeout(() => { console.log("2", name, age, new Date()); resolve("2"); }, 2000); }); }); asyncParallelHook.tapPromise("3", (name, age) => { return new Promise((resolve, reject) => { settimeout(() => { console.log("3", name, age, new Date()); resolve("3"); console.timeEnd("time"); }, 3000); }); }); // 觸發(fā)事件,讓監(jiān)聽函數(shù)執(zhí)行 asyncSeriesHook.promise("panda", 18).then(ret => { console.log(ret); }); // 1 panda 18 2018-08-07T14:45:52.896Z // 2 panda 18 2018-08-07T14:45:54.901Z // 3 panda 18 2018-08-07T14:45:57.901Z // time: 6014.291ms // [ '1', '2', '3' ]
分析上面的執(zhí)行過程,所有的事件處理函數(shù)都返回了 Promise 的實(shí)例,如果想實(shí)現(xiàn) “串行”,則需要讓每一個(gè)返回的 Promise 實(shí)例都調(diào)用 then ,并在 then 中執(zhí)行下一個(gè)事件處理函數(shù),這樣就保證了只有上一個(gè)事件處理函數(shù)執(zhí)行完后才會(huì)執(zhí)行下一個(gè)。
// 模擬 AsyncSeriesHook 類 tapPromise/promise class AsyncSeriesHook { constructor(args) { this.args = args; this.tasks = []; } tapPromise(name, task) { this.tasks.push(task); } promise(...args) { // 傳入?yún)?shù)嚴(yán)格對(duì)應(yīng)創(chuàng)建實(shí)例傳入數(shù)組中的規(guī)定的參數(shù),執(zhí)行時(shí)多余的參數(shù)為 undefined args = args.slice(0, this.args.length); // 將每個(gè)事件處理函數(shù)執(zhí)行并調(diào)用返回 Promise 實(shí)例的 then 方法 // 讓下一個(gè)事件處理函數(shù)在 then 方法成功的回調(diào)中執(zhí)行 let [first, ...others] = this.tasks; return others.reduce((promise, task) => { return promise.then(() => task(...args)); }, first(...args)); } }
上面代碼中的 “串行” 是使用 reduce 歸并來實(shí)現(xiàn)的,首先將存儲(chǔ)所有事件處理函數(shù)的數(shù)組 tasks 解構(gòu)成兩部分,第一個(gè)事件處理函數(shù)和存儲(chǔ)其他事件處理函數(shù)的數(shù)組 others ,對(duì) others 進(jìn)行歸并,將第一個(gè)事件處理函數(shù)執(zhí)行后返回的 Promise 實(shí)例作為歸并的初始值,這樣在歸并的過程中上一個(gè)值始終是上一個(gè)事件處理函數(shù)返回的 Promise 實(shí)例,可以直接調(diào)用 then 方法,并在 then 的回調(diào)中執(zhí)行下一個(gè)事件處理函數(shù),直到歸并完成,將 reduce 最后返回的 Promise 實(shí)例作為 promise 方法的返回值,則實(shí)現(xiàn) promise 方法執(zhí)行后繼續(xù)調(diào)用 then 來實(shí)現(xiàn)后續(xù)邏輯。
對(duì)其他異步鉤子補(bǔ)充
在上面 Async 異步類型的 “鉤子中”,我們只著重介紹了 “串行” 和 “并行”( AsyncParallelHook 和 AsyncSeriesHook )以及回調(diào)和 Promise 的兩種注冊和觸發(fā)事件的方式,還有一些其他的具有一定特點(diǎn)的異步 “鉤子” 我們并沒有進(jìn)行分析,因?yàn)樗麄兊臋C(jī)制與同步對(duì)應(yīng)的 “鉤子” 非常的相似。
AsyncParallelBailHook 和 AsyncSeriesBailHook 分別為異步 “并行” 和 “串行” 執(zhí)行的 “鉤子”,返回值不為 undefined ,即有返回值,則立即停止向下執(zhí)行其他事件處理函數(shù),實(shí)現(xiàn)邏輯可結(jié)合 AsyncParallelHook 、 AsyncSeriesHook 和 SyncBailHook 。
AsyncSeriesWaterfallHook 為異步 “串行” 執(zhí)行的 “鉤子”,上一個(gè)事件處理函數(shù)的返回值作為參數(shù)傳遞給下一個(gè)事件處理函數(shù),實(shí)現(xiàn)邏輯可結(jié)合 AsyncSeriesHook 和 SyncWaterfallHook 。
總結(jié)
在 tapable 源碼中,注冊事件的方法 tab 、 tapSync 、 tapPromise 和觸發(fā)事件的方法 call 、 callAsync 、 promise 都是通過 compile 方法快速編譯出來的,我們本文中這些方法的實(shí)現(xiàn)只是遵照了 tapable 庫這些 “鉤子” 的事件處理機(jī)制進(jìn)行了模擬,以方便我們了解 tapable ,為學(xué)習(xí) Webpack 原理做了一個(gè)鋪墊,在 Webpack 中,這些 “鉤子” 的真正作用就是將通過配置文件讀取的插件與插件、加載器與加載器之間進(jìn)行連接,“并行” 或 “串行” 執(zhí)行,相信在我們對(duì) tapable 中這些 “鉤子” 的事件機(jī)制有所了解之后,再重新學(xué)習(xí) Webpack 的源碼應(yīng)該會(huì)有所頭緒。
聲明:本網(wǎng)頁內(nèi)容旨在傳播知識(shí),若有侵權(quán)等問題請(qǐng)及時(shí)與本網(wǎng)聯(lián)系,我們將在第一時(shí)間刪除處理。TEL:177 7030 7066 E-MAIL:11247931@qq.com