原文出自:https://www.pandashen.com
前言
Webpack 是一個現代 JavaScript 應用程序的靜態模塊打包器,是對前端項目實現自動化和優化必不可少的工具,Webpack 的 loader
(加載器)和 plugin
(插件)是由 Webpack 開發者和社區開發者共同貢獻的,而目前又沒有比較系統的開發文檔,想寫加載器和插件必須要懂 Webpack 的原理,即看懂 Webpack 的源碼,tapable
則是 Webpack 依賴的核心庫,可以說不懂 tapable
就看不懂 Webpack 源碼,所以本篇會對 tapable
提供的類進行解析和模擬。
tapable 介紹
Webpack 本質上是一種事件流的機制,它的工作流程就是將各個插件串聯起來,而實現這一切的核心就是 tapable
,Webpack 中最核心的,負責編譯的 Compiler
和負責創建 bundles
的 Compilation
都是 tapable
構造函數的實例。
打開 Webpack 4.0
的源碼中一定會看到下面這些以 Sync
、Async
開頭,以 Hook
結尾的方法,這些都是 tapable
核心庫的類,爲我們提供不同的事件流執行機制,我們稱爲 “鉤子”。
// 引入 tapable 如下
const {
SyncHook,
SyncBailHook,
SyncWaterfallHook,
SyncLoopHook,
AsyncParallelHook,
AsyncParallelBailHook,
AsyncSeriesHook,
AsyncSeriesBailHook,
AsyncSeriesWaterfallHook
} = require("tapable");
上面的實現事件流機制的 “鉤子” 大方向可以分爲兩個類別,“同步” 和 “異步”,“異步” 又分爲兩個類別,“並行” 和 “串行”,而 “同步” 的鉤子都是串行的。
Sync 類型的鉤子
1、SyncHook
SyncHook
爲串行同步執行,不關心事件處理函數的返回值,在觸發事件之後,會按照事件註冊的先後順序執行所有的事件處理函數。
// SyncHook 鉤子的使用
const { SyncHook } = require("tapable");
// 創建實例
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));
// 觸發事件,讓監聽函數執行
syncHook.call("panda", 18);
// 1 panda 18
// 2 panda 18
// 3 panda 18
在 tapable
解構的 SyncHook
是一個類,註冊事件需先創建實例,創建實例時支持傳入一個數組,數組內存儲事件觸發時傳入的參數,實例的 tap
方法用於註冊事件,支持傳入兩個參數,第一個參數爲事件名稱,在 Webpack 中一般用於存儲事件對應的插件名稱(名字隨意,只是起到註釋作用), 第二個參數爲事件處理函數,函數參數爲執行 call
方法觸發事件時所傳入的參數的形參。
// 模擬 SyncHook 類
class SyncHook {
constructor(args) {
this.args = args;
this.tasks = [];
}
tap(name, task) {
this.tasks.push(task);
}
call(...args) {
// 也可在參數不足時拋出異常
if (args.length < this.args.length) throw new Error("參數不足");
// 傳入參數嚴格對應創建實例傳入數組中的規定的參數,執行時多餘的參數爲 undefined
args = args.slice(0, this.args.length);
// 依次執行事件處理函數
this.tasks.forEach(task => task(...args));
}
}
tasks
數組用於存儲事件處理函數,call
方法調用時傳入參數超過創建 SyncHook
實例傳入的數組長度時,多餘參數可處理爲 undefined
,也可在參數不足時拋出異常,不靈活,後面的例子中就不再這樣寫了。
2、SyncBailHook
SyncBailHook
同樣爲串行同步執行,如果事件處理函數執行時有一個返回值不爲空(即返回值爲 undefined
),則跳過剩下未執行的事件處理函數(如類的名字,意義在於保險)。
// SyncBailHook 鉤子的使用
const { SyncBailHook } = require("tapable");
// 創建實例
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));
// 觸發事件,讓監聽函數執行
syncBailHook.call("panda", 18);
// 1 panda 18
// 2 panda 18
通過上面的用法可以看出,SyncHook
和 SyncBailHook
在邏輯上只是 call
方法不同,導致事件的執行機制不同,對於後面其他的 “鉤子”,也是 call
的區別,接下來實現 SyncBailHook
類。
// 模擬 SyncBailHook 類
class SyncBailHook {
constructor(args) {
this.args = args;
this.tasks = [];
}
tap(name, task) {
this.tasks.push(task);
}
call(...args) {
// 傳入參數嚴格對應創建實例傳入數組中的規定的參數,執行時多餘的參數爲 undefined
args = args.slice(0, this.args.length);
// 依次執行事件處理函數,如果返回值不爲空,則停止向下執行
let i = 0, ret;
do {
ret = this.tasks[i++](...args);
} while (!ret);
}
}
在上面代碼的 call
方法中,我們設置返回值爲 ret
,第一次執行後沒有返回值則繼續循環執行,如果有返回值則立即停止循環,即實現 “保險” 的功能。
3、SyncWaterfallHook
SyncWaterfallHook
爲串行同步執行,上一個事件處理函數的返回值作爲參數傳遞給下一個事件處理函數,依次類推,正因如此,只有第一個事件處理函數的參數可以通過 call
傳遞,而 call
的返回值爲最後一個事件處理函數的返回值。
// SyncWaterfallHook 鉤子的使用
const { SyncWaterfallHook } = require("tapable");
// 創建實例
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"
});
// 觸發事件,讓監聽函數執行
let ret = syncWaterfallHook.call("panda", 18);
console.log("call", ret);
// 1 panda 18
// 2 1
// 3 2
// call 3
SyncWaterfallHook
名稱中含有 “瀑布”,通過上面代碼可以看出 “瀑布” 形象生動的描繪了事件處理函數執行的特點,與 SyncHook
和 SyncBailHook
的區別就在於事件處理函數返回結果的流動性,接下來看一下 SyncWaterfallHook
類的實現。
// 模擬 SyncWaterfallHook 類
class SyncWaterfallHook {
constructor(args) {
this.args = args;
this.tasks = [];
}
tap(name, task) {
this.tasks.push(task);
}
call(...args) {
// 傳入參數嚴格對應創建實例傳入數組中的規定的參數,執行時多餘的參數爲 undefined
args = args.slice(0, this.args.length);
// 依次執行事件處理函數,事件處理函數的返回值作爲下一個事件處理函數的參數
let [first, ...others] = this.tasks;
return reduce((ret, task) => task(ret), first(...args));
}
}
上面代碼中 call
的邏輯是將存儲事件處理函數的 tasks
拆成兩部分,分別爲第一個事件處理函數,和存儲其餘事件處理函數的數組,使用 reduce
進行歸併,將第一個事件處理函數執行後的返回值作爲歸併的初始值,依次調用其餘事件處理函數並傳遞上一次歸併的返回值。
4、SyncLoopHook
SyncLoopHook
爲串行同步執行,事件處理函數返回 true
表示繼續循環,即循環執行當前事件處理函數,返回 undefined
表示結束循環,SyncLoopHook
與 SyncBailHook
的循環不同,SyncBailHook
只決定是否繼續向下執行後面的事件處理函數,而 SyncLoopHook
的循環是指循環執行每一個事件處理函數,直到返回 undefined
爲止,纔會繼續向下執行其他事件處理函數,執行機制同理。
// SyncLoopHook 鉤子的使用
const { SyncLoopHook } = require("tapable");
// 創建實例
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));
// 觸發事件,讓監聽函數執行
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
通過上面的執行結果可以清楚的看到 SyncLoopHook
的執行機制,但有一點需要注意,返回值必須嚴格是 true
纔會觸發循環,多次執行當前事件處理函數,必須嚴格返回 undefined
,纔會結束循環,去執行後面的事件處理函數,如果事件處理函數的返回值不是 true
也不是 undefined
,則會死循環。
在瞭解 SyncLoopHook
的執行機制以後,我們接下來看看 SyncLoopHook
的 call
方法是如何實現的。
// 模擬 SyncLoopHook 類
class SyncLoopHook {
constructor(args) {
this.args = args;
this.tasks = [];
}
tap(name, task) {
this.tasks.push(task);
}
call(...args) {
// 傳入參數嚴格對應創建實例傳入數組中的規定的參數,執行時多餘的參數爲 undefined
args = args.slice(0, this.args.length);
// 依次執行事件處理函數,如果返回值爲 true,則繼續執行當前事件處理函數
// 直到返回 undefined,則繼續向下執行其他事件處理函數
this.tasks.forEach(task => {
let ret;
do {
ret = this.task(...args);
} while (ret === true || !(ret === undefined));
});
}
}
在上面代碼中可以看到 SyncLoopHook
類 call
方法的實現更像是 SyncHook
和 SyncBailHook
的 call
方法的結合版,外層循環整個 tasks
事件處理函數隊列,內層通過返回值進行循環,控制每一個事件處理函數的執行次數。
注意:在 Sync 類型 “鉤子” 下執行的插件都是順序執行的,只能使用 tab
註冊。
Async 類型的鉤子
Async
類型可以使用 tap
、tapAsync
和 tapPromise
註冊不同類型的插件 “鉤子”,分別通過 call
、callAsync
和 promise
方法調用,我們下面會針對 AsyncParallelHook
和 AsyncSeriesHook
的 async
和 promise
兩種方式分別介紹和模擬。
1、AsyncParallelHook
AsyncParallelHook
爲異步並行執行,通過 tapAsync
註冊的事件,通過 callAsync
觸發,通過 tapPromise
註冊的事件,通過 promise
觸發(返回值可以調用 then
方法)。
(1) tapAsync/callAsync
callAsync
的最後一個參數爲回調函數,在所有事件處理函數執行完畢後執行。
// AsyncParallelHook 鉤子:tapAsync/callAsync 的使用
const { AsyncParallelHook } = require("tapable");
// 創建實例
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);
});
// 觸發事件,讓監聽函數執行
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
異步並行是指,事件處理函數內三個定時器的異步操作最長時間爲 3s
,而三個事件處理函數執行完成總共用時接近 3s
,所以三個事件處理函數是幾乎同時執行的,不需等待。
所有 tabAsync
註冊的事件處理函數最後一個參數都爲一個回調函數 done
,每個事件處理函數在異步代碼執行完畢後調用 done
函數,則可以保證 callAsync
會在所有異步函數都執行完畢後執行,接下來看一看 callAsync
是如何實現的。
// 模擬 AsyncParallelHook 類:tapAsync/callAsync
class AsyncParallelHook {
constructor(args) {
this.args = args;
this.tasks = [];
}
tabAsync(name, task) {
this.tasks.push(task);
}
callAsync(...args) {
// 先取出最後傳入的回調函數
let finalCallback = args.pop();
// 傳入參數嚴格對應創建實例傳入數組中的規定的參數,執行時多餘的參數爲 undefined
args = args.slice(0, this.args.length);
// 定義一個 i 變量和 done 函數,每次執行檢測 i 值和隊列長度,決定是否執行 callAsync 的回調函數
let i = 0;
let done = () => {
if (++i === this.tasks.length) {
finalCallback();
}
};
// 依次執行事件處理函數
this.tasks.forEach(task => task(...args, done));
}
}
在 callAsync
中,將最後一個參數(所有事件處理函數執行完畢後執行的回調)取出,並定義 done
函數,通過比較 i
和存儲事件處理函數的數組 tasks
的 length
來確定回調是否執行,循環執行每一個事件處理函數並將 done
作爲最後一個參數傳入,所以每個事件處理函數內部的異步操作完成時,執行 done
就是爲了檢測是不是該執行 callAsync
的回調,當所有事件處理函數均執行完畢滿足 done
函數內部 i
和 length
相等的條件時,則調用 callAsync
的回調。
(2) tapPromise/promise
要使用 tapPromise
註冊事件,對事件處理函數有一個要求,必須返回一個 Promise 實例,而 promise
方法也返回一個 Promise 實例,callAsync
的回調函數在 promise
方法中用 then
的方式代替。
// AsyncParallelHook 鉤子:tapPromise/promise 的使用
const { AsyncParallelHook } = require("tapable");
// 創建實例
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);
});
});
// 觸發事件,讓監聽函數執行
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' ]
上面每一個 tapPromise
註冊事件的事件處理函數都返回一個 Promise 實例,並將返回值傳入 resolve
方法,調用 promise
方法觸發事件時,如果所有事件處理函數返回的 Promise 實例結果都成功,會將結果存儲在數組中,並作爲參數傳遞給 promise
的 then
方法中成功的回調,如果有一個失敗就是將失敗的結果返回作爲參數傳遞給失敗的回調。
// 模擬 AsyncParallelHook 類 tapPromise/promise
class AsyncParallelHook {
constructor(args) {
this.args = args;
this.tasks = [];
}
tapPromise(name, task) {
this.tasks.push(task);
}
promise(...args) {
// 傳入參數嚴格對應創建實例傳入數組中的規定的參數,執行時多餘的參數爲 undefined
args = args.slice(0, this.args.length);
// 將所有事件處理函數轉換成 Promise 實例,併發執行所有的 Promise
return Promise.all(this.tasks.map(task => task(...args)));
}
}
其實根據上面對於 tapPromise
和 promise
使用的描述就可以猜到,promise
方法的邏輯是通過 Promise.all
來實現的。
2、AsyncSeriesHook
AsyncSeriesHook
爲異步串行執行,與 AsyncParallelHook
相同,通過 tapAsync
註冊的事件,通過 callAsync
觸發,通過 tapPromise
註冊的事件,通過 promise
觸發,可以調用 then
方法。
(1) tapAsync/callAsync
與 AsyncParallelHook
的 callAsync
方法類似,AsyncSeriesHook
的 callAsync
方法也是通過傳入回調函數的方式,在所有事件處理函數執行完畢後執行 callAsync
的回調函數。
// AsyncSeriesHook 鉤子:tapAsync/callAsync 的使用
const { AsyncSeriesHook } = require("tapable");
// 創建實例
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);
});
// 觸發事件,讓監聽函數執行
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
異步串行是指,事件處理函數內三個定時器的異步執行時間分別爲 1s
、2s
和 3s
,而三個事件處理函數執行完總共用時接近 6s
,所以三個事件處理函數執行是需要排隊的,必須一個一個執行,當前事件處理函數執行完才能執行下一個。
AsyncSeriesHook
類的 tabAsync
方法註冊的事件處理函數參數中的 next
可以與 AsyncParallelHook
類中 tabAsync
方法參數的 done
進行類比,同爲回調函數,不同點在於 AsyncSeriesHook
與 AsyncParallelHook
的 callAsync
方法的 “並行” 和 “串行” 的實現方式。
// 模擬 AsyncSeriesHook 類:tapAsync/callAsync
class AsyncSeriesHook {
constructor(args) {
this.args = args;
this.tasks = [];
}
tabAsync(name, task) {
this.tasks.push(task);
}
callAsync(...args) {
// 先取出最後傳入的回調函數
let finalCallback = args.pop();
// 傳入參數嚴格對應創建實例傳入數組中的規定的參數,執行時多餘的參數爲 undefined
args = args.slice(0, this.args.length);
// 定義一個 i 變量和 next 函數,每次取出一個事件處理函數執行,並維護 i 的值
// 直到所有事件處理函數都執行完,調用 callAsync 的回調
// 如果事件處理函數中沒有調用 next,則無法繼續
let i = 0;
let next = () => {
let task = this.tasks[i++];
task ? task(...args, next) : finalCallback();
};
next();
}
}
AsyncParallelHook
是通過循環依次執行了所有的事件處理函數,done
方法只爲了檢測是否已經滿足條件執行 callAsync
的回調,如果中間某個事件處理函數沒有調用 done
,只是不會調用 callAsync
的回調,但是所有的事件處理函數都執行了。
而 AsyncSeriesHook
的 next
執行機制更像 Express
和 Koa
中的中間件,在註冊事件的回調中如果不調用 next
,則在觸發事件時會在沒有調用 next
的事件處理函數的位置 “卡死”,即不會繼續執行後面的事件處理函數,只有都調用 next
才能繼續,而最後一個事件處理函數中調用 next
決定是否調用 callAsync
的回調。
(2) tapPromise/promise
與 AsyncParallelHook
類似,tapPromise
註冊事件的事件處理函數需要返回一個 Promise 實例,promise
方法最後也返回一個 Promise 實例。
// AsyncSeriesHook 鉤子:tapPromise/promise 的使用
const { AsyncSeriesHook } = require("tapable");
// 創建實例
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);
});
});
// 觸發事件,讓監聽函數執行
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' ]
分析上面的執行過程,所有的事件處理函數都返回了 Promise 的實例,如果想實現 “串行”,則需要讓每一個返回的 Promise 實例都調用 then
,並在 then
中執行下一個事件處理函數,這樣就保證了只有上一個事件處理函數執行完後纔會執行下一個。
// 模擬 AsyncSeriesHook 類 tapPromise/promise
class AsyncSeriesHook {
constructor(args) {
this.args = args;
this.tasks = [];
}
tapPromise(name, task) {
this.tasks.push(task);
}
promise(...args) {
// 傳入參數嚴格對應創建實例傳入數組中的規定的參數,執行時多餘的參數爲 undefined
args = args.slice(0, this.args.length);
// 將每個事件處理函數執行並調用返回 Promise 實例的 then 方法
// 讓下一個事件處理函數在 then 方法成功的回調中執行
let [first, ...others] = this.tasks;
return others.reduce((promise, task) => {
return promise.then(() => task(...args));
}, first(...args));
}
}
上面代碼中的 “串行” 是使用 reduce
歸併來實現的,首先將存儲所有事件處理函數的數組 tasks
解構成兩部分,第一個事件處理函數和存儲其他事件處理函數的數組 others
,對 others
進行歸併,將第一個事件處理函數執行後返回的 Promise 實例作爲歸併的初始值,這樣在歸併的過程中上一個值始終是上一個事件處理函數返回的 Promise 實例,可以直接調用 then
方法,並在 then
的回調中執行下一個事件處理函數,直到歸併完成,將 reduce
最後返回的 Promise 實例作爲 promise
方法的返回值,則實現 promise
方法執行後繼續調用 then
來實現後續邏輯。
對其他異步鉤子補充
在上面 Async
異步類型的 “鉤子中”,我們只着重介紹了 “串行” 和 “並行”(AsyncParallelHook
和 AsyncSeriesHook
)以及回調和 Promise 的兩種註冊和觸發事件的方式,還有一些其他的具有一定特點的異步 “鉤子” 我們並沒有進行分析,因爲他們的機制與同步對應的 “鉤子” 非常的相似。
AsyncParallelBailHook
和 AsyncSeriesBailHook
分別爲異步 “並行” 和 “串行” 執行的 “鉤子”,返回值不爲 undefined
,即有返回值,則立即停止向下執行其他事件處理函數,實現邏輯可結合 AsyncParallelHook
、AsyncSeriesHook
和 SyncBailHook
。
AsyncSeriesWaterfallHook
爲異步 “串行” 執行的 “鉤子”,上一個事件處理函數的返回值作爲參數傳遞給下一個事件處理函數,實現邏輯可結合 AsyncSeriesHook
和 SyncWaterfallHook
。
總結
在 tapable
源碼中,註冊事件的方法 tab
、tapAsync
、tapPromise
和觸發事件的方法 call
、callAsync
、promise
都是通過 compile
方法快速編譯出來的,我們本文中這些方法的實現只是遵照了 tapable
庫這些 “鉤子” 的事件處理機制進行了模擬,以方便我們瞭解 tapable
,爲學習 Webpack 原理做了一個鋪墊,在 Webpack 中,這些 “鉤子” 的真正作用就是將通過配置文件讀取的插件與插件、加載器與加載器之間進行連接,“並行” 或 “串行” 執行,相信在我們對 tapable
中這些 “鉤子” 的事件機制有所瞭解之後,再重新學習 Webpack 的源碼應該會有所頭緒。