Webpack
的成功之處,不只在於強大的打包構建能力,也在於它靈活的插件機制。javascript
Webpack本質上是一種事件流的機制,它的工做流程就是將各個插件串聯起來,而實現這一切的核心就是Tapable。java
在學習Webpack
的時候,常常能夠看到上述介紹。也就是說學Webpack
的前提是要學習Tapable
。才能更好的學習Webpack
原理。node
其實tapable
的核心思路有點相似於node.js
中的events
,最基本的發佈/訂閱模式。webpack
const EventEmitter = require('events');
const myEmitter = new EventEmitter();
// 註冊事件對應的監聽函數
myEmitter.on('start', (params) => {
console.log("輸出", params)
});
// 觸發事件 並傳入參數
myEmitter.emit('start', '學習webpack工做流'); // 輸出 學習webpack工做流
複製代碼
首先,tapable
提供的鉤子有以下10個。 web
const {
SyncHook,
SyncBailHook,
SyncWaterfallHook,
SyncLoopHook,
AsyncParallelHook,
AsyncParallelBailHook,
AsyncSeriesHook,
AsyncSeriesBailHook,
AsyncSeriesLoopHook,
AsyncSeriesWaterfallHook
} = require("tapable");
複製代碼
其次,全部鉤子的用法簡介,以下:(能夠簡單瞄一眼,就往下看吧)數組
序號 | 鉤子名稱 | 執行方式 | 使用要點 |
---|---|---|---|
1 | SyncHook | 同步串行 | 不關心監聽函數的返回值 |
2 | SyncBailHook | 同步串行 | 只要監聽函數中有一個函數的返回值不爲 undefined,則跳過剩下全部的邏輯 |
3 | SyncWaterfallHook | 同步串行 | 上一個監聽函數的返回值能夠傳給下一個監聽函數 |
4 | SyncLoopHook | 同步循環 | 當監聽函數被觸發的時候,若是該監聽函數返回true時則這個監聽函數會反覆執行,若是返回 undefined 則表示退出循環 |
5 | AsyncParallelHook | 異步併發 | 不關心監聽函數的返回值 |
6 | AsyncParallelBailHook | 異步併發 | 只要監聽函數的返回值不爲 null,就會忽略後面的監聽函數執行,直接跳躍到callAsync等觸發函數綁定的回調函數,而後執行這個被綁定的回調函數 |
7 | AsyncSeriesHook | 異步串行 | 不關心callback()的參數 |
8 | AsyncSeriesBailHook | 異步串行 | callback()的參數不爲null,就會直接執行callAsync等觸發函數綁定的回調函數 |
9 | AsyncSeriesWaterfallHook | 異步串行 | 上一個監聽函數的中的callback(err, data)的第二個參數,能夠做爲下一個監聽函數的參數。 |
10 | AsyncSeriesLoopHook | 異步串行 | 能夠觸發handler循環調用。 |
同步串行,不關心監聽函數的返回值。bash
咱們先來介紹最簡單的SyncHook
,其實每一個Hook
都大同小異,懂一個其餘的就很是好懂了。併發
const {SyncHook} = require("tapable");
//全部的構造函數都接收一個可選的參數,這個參數是一個字符串的數組。
let queue = new SyncHook(['param1']);
// 訂閱tap 的第一個參數是用來標識訂閱的函數的
queue.tap('event 1', function (param1) {
console.log(param1, 1);
});
queue.tap('event 2', function (param1) {
console.log(param1, 2);
});
queue.tap('event 3', function () {
console.log(3);
});
// 發佈的時候觸發訂閱的函數 同時傳入參數
queue.call('hello');
// 控制檯輸出
/* hello 1 hello 2 3 */
複製代碼
能夠看到,這個鉤子訂閱的事件都是按順序同步執行的。app
簡單模擬下原理。異步
class SyncHook{
constructor(){
this.taps = [];
}
// 訂閱
tap(name, fn){
this.taps.push(fn);
}
// 發佈
call(){
this.taps.forEach(tap => tap(...arguments));
}
}
複製代碼
再來看下SyncBailHook
的使用。
只要監聽函數中有一個函數的返回值不爲undefined,則跳過剩下全部的邏輯。
let queue = new SyncBailHook(['param1']); //全部的構造函數都接收一個可選的參數,這個參數是一個字符串的數組。
// 訂閱
queue.tap('event 1', function (param1) {// tap 的第一個參數是用來標識訂閱的函數的
console.log(param1, 1);
return 1;
});
queue.tap('event 2', function (param1) {
console.log(param1, 2);
});
queue.tap('event 3', function () {
console.log(3);
});
// 發佈
queue.call('hello', 'world');// 發佈的時候觸發訂閱的函數 同時傳入參數
// 控制檯輸出
/* hello 1 */
複製代碼
能夠看到,只要監聽函數中有一個函數的返回值不爲undefined
,則跳過剩下全部的邏輯。
簡單模擬下原理。
class SyncBailHook {
constructor() {
this.taps = [];
}
// 訂閱
tap(name, fn) {
this.taps.push(fn);
}
// 發佈
call() {
for (let i = 0, l = this.taps.length; i < l; i++) {
let tap = this.taps[i];
let result = tap(...arguments);
if (result) {
break;
}
}
}
}
複製代碼
上述2
種的鉤子的執行流程以下圖所示:
2
個鉤子的介紹,能夠發現
tapable
提供了各類各樣的
hook
來幫咱們管理事件是如何執行的。
tapable
的核心功能就是控制一系列註冊事件之間的執行流控制,好比我註冊了三個事件,我能夠但願他們是併發的,或者是同步依次執行,又或者其中一個出錯後,後面的事件就不執行了,這些功能均可以經過tapable
的hook
實現。
就像起牀、上班、吃早飯的關係同樣,起牀確定是優先的。可是吃飯和上班就不必定啦。萬一要遲到了呢?可能就放棄早飯了!
記住重點,核心就是call
和tap
兩個方法。
記住重點,核心就是call
和tap
兩個方法。
記住重點,核心就是call
和tap
兩個方法。
那咱們來看下tapable
源碼的SyncHook
是如何實現的,以下。仍是那句話,看完一個,其餘的天然就懂啦。爲了理解,源碼均爲縮減過的,去除了些非核心代碼。
// node_modules/tapable/lib/SyncHook.js
const factory = new SyncHookCodeFactory();
// 繼承基礎Hook類
class SyncHook extends Hook {
// 重寫Hook的compile方法
compile(options) {
// 開發者訂閱的事件傳
factory.setup(this, options);
// 動態生成call方法
return factory.create(options);
}
}
module.exports = SyncHook;
複製代碼
核心代碼很是簡單,能夠看到SyncHook
就是繼承了Hook
基礎類。並重寫了compile
方法。
首先來看下Hook
基礎類的tap
方法。能夠看到每次調用tap
,就是收集當前hook
實例全部訂閱的事件到taps
數組。
// node_modules/tapable/lib/Hook.js
// 訂閱
tap(options, fn) {
// 同步 整理配置項
options = Object.assign({ type: "sync", fn: fn }, options);
// 將訂閱的事件存儲在taps裏面
this._insert(options);
}
_insert(item) {
// 將item 推動 this.taps
this.taps[i] = item;
}
複製代碼
而後來看下Hook
基礎類的call
方法是如何實現的。
// node_modules/tapable/lib/Hook.js
class Hook {
constructor(args) {
this.taps = [];
this.call = this._call;
}
compile(options) {
// 繼承類必須重寫compile
throw new Error("Abstract: should be overriden");
}
// 執行compile生成call方法
_createCall(type) {
return this.compile({
taps: this.taps,
// ...等參數
});
}
}
// 動態生成call方法
function createCompileDelegate(name, type) {
return function lazyCompileHook(...args) {
// 創造call等函數
this[name] = this._createCall(type);
// 執行觸發call等函數
return this[name](...args);
};
}
// 定義_call方法
Object.defineProperties(Hook.prototype, {
_call: {
value: createCompileDelegate("call", "sync"),
configurable: true,
writable: true
},
});
複製代碼
經過上述代碼,咱們能夠發現,call
方法到底是什麼,是經過重寫的compile
方法生成出來的。那咱們再看下compile
方法究竟作了什麼。
先來看下SyncHook
的所有代碼。
// node_modules/tapable/lib/SyncHook.js
const Hook = require("./Hook");
const HookCodeFactory = require("./HookCodeFactory");
// 繼承工廠類
class SyncHookCodeFactory extends HookCodeFactory {
// call方法個性化定製
content({ onError, onDone, rethrowIfPossible }) {
return this.callTapsSeries({
onError: (i, err) => onError(err),
onDone,
rethrowIfPossible
});
}
}
const factory = new SyncHookCodeFactory();
// 繼承基礎Hook類
class SyncHook extends Hook {
// 重寫Hook的compile方法
compile(options) {
// 開發者訂閱的事件傳
factory.setup(this, options);
// 動態生成call方法
return factory.create(options);
}
}
module.exports = SyncHook;
複製代碼
能夠看到compile
主要是執行factory
的方法,而factory
是SyncHookCodeFactory
的實例,繼承了HookCodeFactory
類,而後factory
實例調用了setup
方法。
setup
就是將taps
中訂閱的事件方法統一給了this._x
;
// node_modules/tapable/lib/HookCodeFactory.js
setup(instance, options) {
// 將taps裏的全部fn 賦值給 _x
instance._x = options.taps.map(t => t.fn);
}
複製代碼
而後再看下factory
實例調用的create
方法。
// node_modules/tapable/lib/HookCodeFactory.js
create(options) {
this.init(options);
let fn;
switch (this.options.type) {
case "sync":
fn = new Function(
// 參數
this.args(),
// 函數體
'"use strict";\n' +
// 獲取一些須要的變量
this.header() +
// 事件運行邏輯
this.content({
onError: err => `throw ${err};\n`,
onResult: result => `return ${result};\n`,
resultReturns: true,
onDone: () => "",
rethrowIfPossible: true
})
);
break;
}
}
複製代碼
create
會將傳進來的全部事件,進行組裝。最終生成call
方法。 以下就是咱們此次的案例最終生成的call
方法。
function anonymous(param1) {
"use strict";
var _context;
var _x = this._x;
var _fn0 = _x[0];
_fn0(param1);
var _fn1 = _x[1];
_fn1(param1);
var _fn2 = _x[2];
_fn2(param1);
}
複製代碼
若是你訂閱了5
個事件,上述代碼就會變成5
個函數的依次執行。以及參數必須是建立hook
實例就聲明好的。不然tap
事件傳的參數是無用的~
以上代碼仍是簡寫了不少,你們能夠直接去看下源碼,很是精簡好理解。給做者大大點贊。👍
總結一下,核心就是call
和tap
兩個方法。其實還有tapAsync
等...可是原理都是同樣的。tap
收集訂閱的事件,觸發call
方法時根據hook
的種類動態生成對應的執行體。以下圖,其餘hook
的實現也是同理。
Webpack
的流程能夠分爲如下三大階段:
執行webpack
時,會生成一個compiler
實例。
// node_modules/webpack/lib/webpack.js
const Compiler = require("./Compiler");
const MultiCompiler = require("./MultiCompiler");
const webpack = (options, callback) => {
// ...省略了多餘代碼...
let compiler;
if (typeof options === "object") {
compiler = new Compiler(options.context);
} else {
throw new Error("Invalid argument: options");
}
})
複製代碼
咱們發現Compiler
是繼承了Tapable
的。同時發現webpack
的生命週期hooks
都是各類各樣的鉤子。
// node_modules/webpack/lib/Compiler.js
class Compiler extends Tapable {
constructor(context) {
super();
this.hooks = {
/** @type {AsyncSeriesHook<Stats>} */
done: new AsyncSeriesHook(["stats"]),
/** @type {AsyncSeriesHook<>} */
additionalPass: new AsyncSeriesHook([]),
/** @type {AsyncSeriesHook<Compiler>} */
beforeRun: new AsyncSeriesHook(["compiler"]),
/** @type {AsyncSeriesHook<Compiler>} */
run: new AsyncSeriesHook(["compiler"]),
/** @type {AsyncSeriesHook<Compilation>} */
emit: new AsyncSeriesHook(["compilation"]),
/** @type {AsyncSeriesHook<string, Buffer>} */
assetEmitted: new AsyncSeriesHook(["file", "content"]),
/** @type {AsyncSeriesHook<Compilation>} */
afterEmit: new AsyncSeriesHook(["compilation"]),
// ....等等等不少 你們看下源碼吧.... 不看也沒有關係
}
}
}
複製代碼
而後在初始化webpack
的配置過程當中,會循環咱們配置的以及webpack
默認的全部插件也就是plugin
。
// 訂閱在options中的全部插件
if (options.plugins && Array.isArray(options.plugins)) {
for (const plugin of options.plugins) {
if (typeof plugin === "function") {
plugin.call(compiler, compiler);
} else {
plugin.apply(compiler);
}
}
}
複製代碼
這個過程,會把plugin
中全部tap
事件收集到每一個生命週期的hook
中。 最後根據每一個hook
執行call
方法的順序(也就是生命週期)。就能夠把全部plugin
執行了。
舉個例子,下面是咱們常用的熱更新插件代碼,它訂閱了additionalPass
等hook
。
webpack
它工做流程能將各個插件
plugin
串聯起來的緣由,而實現這一切的核心就是
Tapable
。
雖然插件化設計很靈活,咱們能夠寫插件操做webpack
的整個生命週期。可是也發現插件化設計帶來的一些問題,就是閱讀源碼很是很差的體驗:
(1)聯繫鬆散。使用tapable
鉤子相似事件監聽模式,雖然能有效解耦,但鉤子的註冊與調用幾乎沒有聯繫。
(2)看到源碼裏一個模塊提供了幾個鉤子,但並不知道,在什麼時候、何地該鉤子會被調用,又在什麼時候、何地鉤子上被註冊了哪些方法。這些以往都是須要經過在代碼庫中搜索關鍵詞來解決。
(3)鉤子數量衆多。webpack
內部的鉤子很是多,數量達到了180+
,
本篇文主要是講原理,理解tapable
。其餘的鉤子的使用,能夠看這篇文章。