Webpack
本質上是一種事件流的機制,它的工做流程就是將各個插件串聯起來,而實現這一切的核心就是 Tapable
,Webpack
中最核心的負責編譯的 Compiler
和負責建立 bundles
的 Compilation
都是 Tapable
的實例,而且實例內部的生命週期也是經過 Tapable
庫提供的鉤子類實現的。javascript
class Compiler extends Tapable {
constructor(context) {
super();
this.hooks = {
shouldEmit: new SyncBailHook(["compilation"]),
done: new AsyncSeriesHook(["stats"]),
additionalPass: new AsyncSeriesHook([]),
beforeRun: new AsyncSeriesHook(["compiler"]),
run: new AsyncSeriesHook(["compiler"]),
emit: new AsyncSeriesHook(["compilation"]),
assetEmitted: new AsyncSeriesHook(["file", "content"]),
afterEmit: new AsyncSeriesHook(["compilation"]),
thisCompilation: new SyncHook(["compilation", "params"]),
compilation: new SyncHook(["compilation", "params"]),
normalModuleFactory: new SyncHook(["normalModuleFactory"]),
contextModuleFactory: new SyncHook(["contextModulefactory"]),
beforeCompile: new AsyncSeriesHook(["params"]),
compile: new SyncHook(["params"]),
make: new AsyncParallelHook(["compilation"]),
afterCompile: new AsyncSeriesHook(["compilation"]),
watchRun: new AsyncSeriesHook(["compiler"]),
failed: new SyncHook(["error"]),
invalid: new SyncHook(["filename", "changeTime"]),
watchClose: new SyncHook([]),
infrastructureLog: new SyncBailHook(["origin", "type", "args"]),
environment: new SyncHook([]),
afterEnvironment: new SyncHook([]),
afterPlugins: new SyncHook(["compiler"]),
afterResolvers: new SyncHook(["compiler"]),
entryOption: new SyncBailHook(["context", "entry"])
};
}
}複製代碼
咱們知道 Node.js
的特色是事件驅動,它是經過內部的 EventEmitter
類實現的,這個類可以進行事件的監聽與觸發。java
const { EventEmitter } = require('events');
const event = new EventEmitter();
event.on('eventName', value => {
console.log('eventName 觸發:', value);
});
event.emit('eventName', 'Hello, eventName');複製代碼
Tapable
的功能與 EventEmitter
相似,可是更增強大,它包含了多種不一樣的監聽和觸發事件的方式。node
經過上文 Compiler
類內部能看到 Tapable
提供的類都是給生命週期實例化的,所以咱們叫它鉤子類。webpack
Tapable
導出的鉤子類:git
const {
SyncHook,
SyncBailHook,
SyncWaterfallHook,
SyncLoopHook,
AsyncSeriesHook,
AsyncSeriesBailHook,
AsyncSeriesWaterfallHook,
AsyncParallelHook,
AsyncParallelBailHook,
} = require('tapable');複製代碼
Hook 的類型能夠按照 事件回調的運行邏輯 或者 觸發事件的方式 來分類。github
事件回調的運行邏輯:web
類型npm |
描述json |
Basicsegmentfault |
基礎類型,單純的調用註冊的事件回調,並不關心其內部的運行邏輯。 |
Bail |
保險類型,當一個事件回調在運行時返回的值不爲 |
Waterfall |
瀑布類型,若是當前執行的事件回調返回值不爲 |
Loop |
循環類型,若是當前執行的事件回調的返回值不是 |
觸發事件的方式:
類型 |
描述 |
Sync |
Sync 開頭的 Hook 類只能用 |
AsyncSeries |
Async 開頭的 Hook 類,無法用 |
AsyncParalle |
在開始對源碼進行解析以前,咱們首先來看下 Tapable
一些重要的使用方式。
註冊事件回調有三個方法: tap
、tapAsync
和 tapPromise
,其中 tapAsync
和 tapPromise
不能用於 Sync 開頭的鉤子類,強行使用會報錯。tapAsync
和 tapPromise
與 tap
的使用方法相似,我單獨以 tap
舉例。
const { SyncHook } = require('tapable');
const hook = new SyncHook();
// 註冊事件回調
// 註冊事件回調的方法,例如 tap,它們的第一個參數能夠是事件回調的名字,也能夠是配置對象
hook.tap('first', () => {
console.log('first');
});
hook.tap(
// 配置對象
{
name: 'second',
},
() => {
console.log('second');
}
);複製代碼
在註冊事件回調時,配置對象有兩個能夠改變執行順序的屬性:
stage
:這個屬性的類型是數字,數字越大事件回調執行的越晚。const { SyncHook } = require('tapable');
const hook = new SyncHook();
hook.tap('first', () => {
console.log('first');
});
hook.tap({
name: 'second',
// 默認 stage 是 0,會按註冊順序添加事件回調到隊列尾部
// 順序提早,stage 能夠置爲負數(比零小)
// 順序提後,stage 能夠置爲正數(比零大)
stage: 10,
}, () => {
console.log('second');
});
hook.tap('third', () => {
console.log('third');
});
hook.call('call');
/** * Console output: * * first * third * second */複製代碼
before
:這個屬性的類型能夠是數組也能夠是一個字符串,傳入的是註冊事件回調的名稱。const { SyncHook } = require('tapable');
const hook = new SyncHook();
hook.tap('first', (name) => {
console.log('first', name);
});
hook.tap('second', (name) => {
console.log('second', name);
});
hook.tap({
name: 'third',
// 把 third 事件回調放到 second 以前執行
before: 'second',
}, (name) => {
console.log('third', name);
});
hook.call('call');
/** * Console output: * * first * third * second */複製代碼
另外,這兩個屬性最好不要同時使用,比較容易混亂。
觸發事件的三個方法是與註冊事件回調的方法一一對應的,這點從方法的名字上也能看出來:call
對應 tap
、callAsync
對應 tapAsync
和 promise
對應 tapPromise
。通常來講,咱們註冊事件回調時用了什麼方法,觸發時最好也使用對應的方法。
call
傳入參數的數量須要與實例化時傳遞給鉤子類構造函數的數組長度保持一致。
const { SyncHook } = require('tapable');
// 1.實例化鉤子類時傳入的數組,實際上只用上了數組的長度,名稱是爲了便於維護
const hook = new SyncHook(['name']);
// 3.other 會是 undefined,由於這個參數並無在實例化鉤子類的數組中聲明
hook.tap('first', (name, other) => {
console.log('first', name, other);
});
// 2.實例化鉤子類的數組長度爲 1,這裏卻傳了 2 個傳入參數
hook.call('call', 'test');
/** * Console output: * * first call undefined */複製代碼
callAsync
與 call
不一樣的是:在傳入了與實例化鉤子類的數組長度一致個數的傳入參數時,還須要在最後添加一個回調函數,不然在事件回調中執行回調函數可能會報錯。
const { AsyncSeriesHook } = require('tapable');
const hook = new AsyncSeriesHook(['name']);
// 事件回調接收到 callback
hook.tapAsync('first', (name, callback) => {
console.log('first', name, callback);
callback();
});
// 最後一個傳入參數是回調函數
hook.callAsync('callAsync', (error) => {
console.log('callAsync', error);
});
/** * Console output: * * first callAsync [Function] * callAsync first */複製代碼
另外,事件回調中接收的 callback
必需要執行,不然不會執行後續的事件回調和 callAsync
傳入的回調,這是由於事件回調接收的 callback
已是對 callAsync
傳入的回調作了一層封裝的結果了,其內部有一個判斷邏輯:
callAsync
傳入的回調,再也不執行後續的事件回調;這實際上意味着事件回調執行有錯誤,也就是說 callAsync
傳入的是一個錯誤優先回調,既然是錯誤優先回調,那它是能夠接收第二個參數的,這個參數將被傳入正確的值,在這邊先不用管第二個參數,下文會有更詳盡的介紹。hook.tapAsync('first', (name, callback) => {
console.log('first', name, callback);
// 繼續執行 second 事件回調
callback();
});
hook.tapAsync('second', (name, callback) => {
console.log('second', name, callback);
// 執行 callAsync 傳入的回調
// 第二個參數傳入沒有效果,由於 Sync 類型的 Hook 不對第二個參數作處理
callback('second error', 'second result');
});
hook.tapAsync('third', (name, callback) => {
console.log('third', name, callback);
callback('third');
});
// 錯誤優先回調
// result 打印 undefined
hook.callAsync('callAsync', (error, result) => {
console.log('callAsync', error, result);
});
/** * Console output: * * first callAsync [Function] * second callAsync [Function] * callAsync second error undefined */複製代碼
promise
執行以後會返回一個 Promise
對象。在使用 tapPromise
註冊事件回調時,事件回調必須返回一個 Promise
對象,不然會報錯,這是爲了確保事件回調可以按照順序執行。
const { AsyncSeriesHook } = require('tapable');
const hook = new AsyncSeriesHook(['name']);
hook.tapPromise('first', (name) => {
console.log('first', name);
return Promise.resolve('first');
});
hook.tapPromise('second', (name) => {
console.log('second', name);
return Promise.resolve('second');
});
const promise = hook.promise('promise');
console.log(promise);
promise.then(value => {
// value 是 undefined,不會接收到事件回調中傳入的值
console.log('value', value);
}, reason => {
// 事件回調返回的 Promise 對象狀態是 Rejected
// reason 會有事件回調中傳入的錯誤信息
console.log('reason', reason);
});
/** * Console output: * * first promise * Promise { <pending> } * second promise * value undefined */複製代碼
咱們能夠給鉤子類添加攔截器,這樣就能對事件回調的註冊、調用以及事件的觸發進行監聽。
const { SyncHook } = require('tapable');
const hook = new SyncHook();
hook.intercept({
// 註冊時執行
register(tap) {
console.log('register', tap);
return tap;
},
// 觸發事件時執行
call(...args) {
console.log('call', args);
},
// 在 call 攔截器以後執行
loop(...args) {
console.log('loop', args);
},
// 事件回調調用前執行
tap(tap) {
console.log('tap', tap);
},
});複製代碼
用 tap
或者其餘方法註冊事件回調以及添加攔截器時,能夠把配置對象中的 context
設置爲 true
,這將讓咱們在事件回調或者攔截器方法中獲取 context
對象,這個對象會變成它們的第一個參數。
const { SyncHook } = require('tapable');
// 鉤子類的構造函數接收一個數組做爲參數,數組中是事件回調的參數名,代表事件回調須要幾個參數
const hook = new SyncHook(['name']);
hook.intercept({
// 在添加攔截器的配置對象中啓用 context
context: true,
register(tap) {
console.log('register', tap);
return tap;
},
call(...args) {
// args[0] 會變成 context 對象
console.log('call', args);
},
loop(...args) {
// args[0] 會變成 context 對象
console.log('loop', args);
},
tap(context, tap) {
// 第一個參數變成 context 對象
context.fileChanged = true;
console.log('tap', context, tap);
},
});
hook.tap(
{
name: 'first',
context: true,
},
// 第一個參數變成 context 對象
(context, name) => {
// context 中將會有 fileChanged 屬性
// context: { fileChanged: true }
console.log('first', context, name);
}
);
hook.call('call');
/** * Console output: * * register { type: 'sync', fn: [Function], name: 'first', context: true } * call [ {}, 'call' ] * tap { fileChanged: true } { type: 'sync', fn: [Function], name: 'first', context: true } * first { fileChanged: true } call */複製代碼
Tapable
暴露的全部鉤子類都是繼承自 Hook
的,所以它們的構造函數統一隻接收一個數組參數,這個數組中是事件回調的參數名,主要做用是代表事件回調須要幾個參數。
接下來我會着重介紹 SyncHook
、AsyncSeriesBailHook
、Async
Series
WaterfallHook
、SyncLoopHook
、AsyncParallelHook
、AsyncParallelBailHook
這六個鉤子類,其餘鉤子的用法與它們相似。
Basic 類型的鉤子類很簡單就是按照順序執行事件回調,沒有任何其餘功能。
const { SyncHook } = require('tapable');
const hook = new SyncHook(['name']);
// 註冊事件回調
hook.tap('first', name => {
console.log('first', name);
});
hook.tap('second', name => {
console.log('second', name);
});
// 觸發事件
hook.call('call');
/** * Console output: * * first call * second call */複製代碼
Bail 類型的鉤子類在事件回調有返回值時,會終止後續事件回調的運行,可是這隻對 tap
方法有效,下面來看下不一樣的註冊事件回調的方法是怎麼觸發這一功能的。
const { AsyncSeriesBailHook } = require('tapable');
const hook = new AsyncSeriesBailHook(['name']);
hook.tap('first', (name) => {
console.log('first', name);
// return 不爲 undefined 的值
// return 'first return';
/** * Console output: * * first callAsync * end null first return */
})
hook.tapAsync('second', (name, callback) => {
console.log('second', name);
// callback 的第一個參數須要傳入 null,代表沒有錯誤;
// 第二個參數須要傳入不爲 undefined 的值;
// 這即是錯誤優先回調的標準格式。
// callback(null, 'second return');
/** * Console output: * * first callAsync * second callAsync * end null second return */
callback();
})
hook.tapPromise('third', (name, callback) => {
console.log('third', name);
// Promise 最終狀態被置爲 Fulfilled,而且值不爲 undefined
// return Promise.resolve('third return');
/** * Console output: * * first callAsync * second callAsync * third callAsync * end null third return */
return Promise.resolve();
})
hook.tap('fourth', (name) => {
console.log('fourth', name);
})
hook.callAsync('callAsync', (error, result) => {
console.log('end', error, result);
});
// 使用 promise 方法觸發事件,事件回調中也是用同樣的方式來中止後續事件回調執行的;
// 區別主要在於處理錯誤和值的方式而已,這即是異步回調和 Promise 的不一樣之處了,
// 並不在本文探討範圍以內。
// const promise = hook.promise('promise');
// promise.then(value => {
// console.log('value', value);
// }, reason => {
// console.log('reason', reason);
// });複製代碼
Waterfall 類型的鉤子類在當前事件回調返回不爲 undefined
的值時,會把下一個事件回調的第一個參數替換成這個值,固然這也是針對 tap
註冊的事件回調,其餘註冊方法觸發這一功能的方式以下:
const { AsyncSeriesWaterfallHook } = require('tapable');
const hook = new AsyncSeriesWaterfallHook(['name']);
hook.tap('first', name => {
console.log('first', name);
// 返回不爲 undefined 的值
return name + ' - ' + 'first';
})
hook.tapAsync('second', (name, callback) => {
// 由於 tap 註冊的事件回調返回了值,因此 name 爲 callAsync - first
console.log('second', name);
// 在第二個參數中傳入不爲 undefined 的值
callback(null, name + ' - ' + ' second');
})
hook.tapPromise('third', name => {
console.log('third', name);
// Promise 最終狀態被置爲 Fulfilled,而且值不爲 undefined
return Promise.resolve(name + ' - ' + 'third');
})
hook.tap('fourth', name => {
// 當前事件回調沒有返回不爲 undefined 的值,所以 name 沒有被替換
console.log('fourth', name);
})
hook.callAsync('callAsync', (error, result) => {
console.log('end', error, result);
});
/** * Console output: * * first callAsync * second callAsync - first * third callAsync - first - second * fourth callAsync - first - second - third * end null callAsync - first - second - third */複製代碼
Loop 類型的鉤子類在當前執行的事件回調的返回值不是 undefined
時,會從新從第一個註冊的事件回調處執行,直到當前執行的事件回調沒有返回值。在下面的代碼中,我作了一些處理,使得它的打印值更爲直觀。
const { SyncLoopHook } = require('tapable');
const hook = new SyncLoopHook(['name']);
const INDENT_SPACE = 4;
let firstCount = 0;
let secondCount = 0;
let thirdCount = 0;
let indent = 0;
function indentLog(...text) {
console.log(new Array(indent).join(' '), ...text);
}
hook.tap('first', name => {
if (firstCount === 1) {
firstCount = 0;
indent -= INDENT_SPACE;
indentLog('</callback-first>');
return;
}
firstCount++;
indentLog('<callback-first>');
indent += INDENT_SPACE;
return true;
})
hook.tap('second', name => {
if (secondCount === 1) {
secondCount = 0;
indent -= INDENT_SPACE;
indentLog('</callback-second>');
return;
}
secondCount++;
indentLog('<callback-second>');
indent += INDENT_SPACE;
return true;
})
hook.tap('third', name => {
if (thirdCount === 1) {
thirdCount = 0;
indent -= INDENT_SPACE;
indentLog('</callback-third>');
return;
}
thirdCount++;
indentLog('<callback-third>');
indent += INDENT_SPACE;
return true;
})
hook.call('call');
/** * Console output: * * <callback-first> * </callback-first> * <callback-second> * <callback-first> * </callback-first> * </callback-second> * <callback-third> * <callback-first> * </callback-first> * <callback-second> * <callback-first> * </callback-first> * </callback-second> * </callback-third> */複製代碼
AsyncParallel 類型的鉤子類會串行執行全部的事件回調,所以異步的事件回調中的錯誤並不會阻止其餘事件回調的運行。
const { AsyncParallelHook } = require('tapable');
const hook = new AsyncParallelHook(['name']);
hook.tap('first', (name) => {
console.log('first', name);
})
hook.tapAsync('second', (name, callback) => {
setTimeout(() => {
console.log('second', name);
callback();
}, 2000);
})
hook.tapPromise('third', (name) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log('third', name);
// 拋出了錯誤,可是隻是提早執行了 callAsync 傳入回調函數,並不會阻止其餘事件回調運行
reject('third error');
}, 1000);
});
})
hook.callAsync('callAsync', (error) => {
console.log('end', error);
});
/** * Console output: * * first callAsync * third callAsync * end third error * second callAsync */複製代碼
這個類型的鉤子類看起來很讓人疑惑,以 AsyncParallel 開頭的鉤子類會串行執行全部事件回調,而 Bail 類型的鉤子類在事件回調返回不爲 undefined
時會終止後續事件回調的運行,這兩個結合起來要怎麼使用呢?
實際上,AsyncParallelBailHook 確實會串行執行全部事件回調,可是這個鉤子類中的事件回調返回值若是不爲 undefined
,那麼 callAsync
傳入的回調函數的第二參數會是最早擁有返回值(這裏的返回值有多種方式:return result
、callback(null, result)
和 return Promise.resolve(result)
)邏輯的事件回調的那個返回值,看以下代碼:
const { AsyncParallelBailHook } = require('tapable');
const hook = new AsyncParallelBailHook(['name']);
hook.tap('first', (name) => {
console.log('first', name);
})
// 最早擁有返回值邏輯的事件回調
hook.tapAsync('second', (name, callback) => {
setTimeout(() => {
console.log('second', name);
// 使用 callback 傳入了不是 undefined 的返回值。
callback(null, 'second result');
}, 1000);
})
// 雖然這個異步的事件回調中的 Promise 對象會比第二個異步的事件回調早執行完畢,
// 可是由於第二個事件回調中已經擁有了返回值的邏輯,
// 所以這個事件回調不會執行 callAsync 傳入的回調函數。
hook.tapPromise('third', (name) => {
console.log('third', name);
// 返回了一個 Promise 對象,而且它的狀態是 Fulfilled, 值不爲 undefined。
return Promise.resolve('third result');
})
hook.callAsync('callAsync', (error, result) => {
console.log('end', error, result);
});
/** * Console output: * * first callAsync * third callAsync * second callAsync * end null second result */複製代碼
經過上文咱們已經大體瞭解了 Tapable
的使用方式,接下來咱們來看下 Tapable
到底是如何運做的。要探尋這個祕密,咱們須要從 tap
和 call
這兩個方法開始分析,這兩個方法都是創建在同步的前提下,所以會簡單一些。
另外,咱們說過全部的鉤子類都是繼承自 Hook
類,可是 Tapable
並無暴露它而且它也無法直接使用,所以下面主要把 SyncHook
鉤子類做爲入口進行解析。在解析的過程當中,咱們也須要在本地經過 npm
安裝 Tapable
庫,並寫一些簡單的 DEMO
進行調試,以便於理解。
// 安裝 Tapable
npm i -D tapable複製代碼
// index.js
// DEMO
const { SyncHook } = require('tapable');
const hook = new SyncHook(['name']);
hook.tap('run', (name) => {
console.log('run', name);
});
hook.call('call');複製代碼
Tapable
的版本問題:寫文章時
Tapable
最新的latest
版本是 1.1.3,可是Webpack
團隊已經在開發 2.0.0 版本,如今是beta
階段,可能 API 還會有所變更,而且就目前來看beta
版本對比 1.0.0 版本也沒啥大的改動,因此文章依舊選用 1.1.3 版本進行解析。
經過查看 Tapable
庫的 package.json
,能夠找到 main: lib/index.js
。下面是這個文件的代碼:
// tapable/lib/index.js
...
exports.SyncHook = require("./SyncHook");
...複製代碼
能夠看到 SyncHook
是從 SyncHook.js
中暴露出來的,SyncHook
類中暫時沒有發現什麼有價值的代碼,可是我能夠看到 tapAsync
和 tapPromise
被重寫了,內部是拋出錯誤的邏輯,所以解釋了 SyncHook
類爲何不容許執行這兩個方法。
// tapable/lib/SyncHook.js
...
class SyncHook extends Hook {
tapAsync() {
throw new Error("tapAsync is not supported on a SyncHook");
}
tapPromise() {
throw new Error("tapPromise is not supported on a SyncHook");
}
compile(options) {
factory.setup(this, options);
return factory.create(options);
}
}
...複製代碼
咱們要使用鉤子類,那必需要先進行實例化。SyncHook
中代碼很簡單,大部分邏輯都繼承了 Hook
,咱們繼續向上追蹤。下面是 Hook
實例化的代碼,雖然給予了註釋,可是還有一些是須要結合詳細流程來看的代碼,下文有詳細解析,所以暫時沒必要理會。
// tapable/lib/Hook.js
...
class Hook {
constructor(args) {
if (!Array.isArray(args)) args = [];
// 事件回調的接收參數名數組,主要用到了數組的長度,
// 由於須要知道傳入了幾個參數,因此參數名主要是爲了便於維護。
this._args = args;
// 註冊的事件回調都會放到這個數組中,數組裏面已是排序好的事件回調。
this.taps = [];
// 保存着攔截器配置對象。
this.interceptors = [];
// 下面三個觸發事件的方法都通過了一層封裝。
this.call = this._call;
this.promise = this._promise;
this.callAsync = this._callAsync;
// 拼接代碼時使用。
this._x = undefined;
}
...
}
...複製代碼
實例化以後,咱們就要正式開始使用了,首先確定要先註冊事件回調,以後觸發事件纔有意義。下面是 tap
方法在 Hook
中的代碼:
// tapable/lib/Hook.js
...
class Hook {
...
tap(options, fn) {
// tap 的第一個參數能夠是當前事件回調的名字,也能夠是一個配置對象,
// 下面是對這個的處理和一些容錯。
if (typeof options === "string") options = { name: options };
if (typeof options !== "object" || options === null)
throw new Error(
"Invalid arguments to tap(options: Object, fn: function)"
);
// 最後把 options 和 fn 等都合併到一個對象中去,
// 其中有一個 type 屬性,在以後的處理時會根據 type 的不一樣觸發不一樣的邏輯
options = Object.assign({ type: "sync", fn: fn }, options);
if (typeof options.name !== "string" || options.name === "")
throw new Error("Missing name for tap");
options = this._runRegisterInterceptors(options);
this._insert(options);
}
_runRegisterInterceptors(options) {
for (const interceptor of this.interceptors) {
if (interceptor.register) {
// options 若是在 register 攔截器中從新返回,那它就會把 options 替換掉
const newOptions = interceptor.register(options);
if (newOptions !== undefined) options = newOptions;
}
}
return options;
}
_resetCompilation() {
this.call = this._call;
this.callAsync = this._callAsync;
this.promise = this._promise;
}
_insert(item) {
// 重置三個調用事件的方法,暫時不用管它,解析完觸發流程以後就會知道它的做用。
this._resetCompilation();
let before;
// 若是 before 屬性存在,把它轉換成 Set 數據結構
if (typeof item.before === "string") before = new Set([item.before]);
else if (Array.isArray(item.before)) {
before = new Set(item.before);
}
let stage = 0;
if (typeof item.stage === "number") stage = item.stage;
let i = this.taps.length;
while (i > 0) {
i--;
const x = this.taps[i];
// 第一次遍歷會添加到數組尾部。
// taps 數組中每次都會存在相鄰的兩個相同的值,
// 靠後的下標就是最後要被賦值的下標。
this.taps[i + 1] = x;
const xStage = x.stage || 0;
// 若是碰到傳入 before 中有當前 name 的,就繼續遍歷,直到把 before 所有清空。
if (before) {
if (before.has(x.name)) {
before.delete(x.name);
continue;
}
// 若是 before 中的值沒被刪乾淨,
// 新加入的事件回調最終會在最前面執行。
if (before.size > 0) {
continue;
}
}
// 若是當前 stage 大於傳入的 stage,那麼繼續遍歷。
if (xStage > stage) {
continue;
}
i++;
break;
}
// 循環結束的時候 i 已是要賦值的那個下標了。
this.taps[i] = item;
}
}
...
}
...複製代碼
註冊流程並無什麼特殊之處,主要目的無非是把包含事件回調的配置對象放入一個數組中存儲並進行排序;而下來的觸發流程,其主體思想是執行編譯拼接成的靜態腳本,這樣可能會更加快速,具體能夠看 #86。
經過上文咱們瞭解到 call
方法是在 Hook
類中定義的,在 Hook
類的構造函數中咱們看到 call
方法的值是 _call
方法。
// tapable/lib/Hook.js
...
class Hook {
constructor(args) {
...
this.call = this._call;
...
}
...
}
...複製代碼
在 Hook
類文件的最下方找到下面代碼,會發現 _call
方法是經過 Object.defineProperties
定義到 Hook.prototype
上的,它的值是經過 createCompileDelegate
函數返回的。
// tapable/lib/Hook.js
...
function createCompileDelegate(name, type) {
return function lazyCompileHook(...args) {
this[name] = this._createCall(type);
return this[name](...args);
};
}
Object.defineProperties(Hook.prototype, {
_call: {
value: createCompileDelegate("call", "sync"),
configurable: true,
writable: true
},
...
});複製代碼
createCompileDelegate
函數返回的最終結果:
this._call = function lazyCompileHook(...args) {
this.call = this._createCall('sync');
return this.call(...args);
};複製代碼
在 _call
方法執行以後,會去調用 _createCall
方法,_createCall
方法內部又會調用 compile
方法。
// tapable/lib/Hook.js
...
class Hook {
...
compile(options) {
// 提示必須在子類中重寫 compile 方法
throw new Error("Abstract: should be overriden");
}
_createCall(type) {
return this.compile({
taps: this.taps,
interceptors: this.interceptors,
args: this._args,
type: type
});
}
...
}
...複製代碼
可是,咱們看到 Hook
類中的 compile
方法裏面是拋出錯誤的邏輯,提示咱們必需要在子類中重寫這個方法,所以咱們須要到 SyncHook
類中查看重寫 Hook
類的 compile
方法。
// tapable/lib/SyncHook.js
...
class SyncHook extends Hook {
...
compile(options) {
factory.setup(this, options);
return factory.create(options);
}
}
...複製代碼
能夠看到 compile
內部使用了 factory.create
來返回值,到此咱們先停一停,回過頭來看 Hook
類中的 _createCall
方法,它的返回值(compile
咱們並無分析完,可是從它的名字也能看出來,它會編譯生成靜態腳本)最終會賦值給 call
方法,也就是說在第二次及以後執行 call
方法會直接執行已經編譯好的靜態腳本,這裏用到了惰性函數來優化代碼的運行性能。
class Hook {
constructor(args) {
// 第一次執行的時候 call 仍是等於 _call 的。
this.call = this._call;
}
}
this._call = function lazyCompileHook(...args) {
// 第二次執行的時候,call 已是 this._createCall(...) 返回的已經編譯好的靜態腳本了。
this.call = this._createCall('sync');
return this.call(...args);
};複製代碼
至 compile
方法爲止,咱們來看下 call
方法的流程圖:
另外,咱們在解析註冊流程時,在添加事件回調的 _insert
方法開頭處看到了 _resetCompilation
方法,當時並無談到它的做用,可是在大體解析了 call
方法以後,咱們能夠談一談了。
...
class Hook {
...
_resetCompilation() {
this.call = this._call;
...
}
_insert(item) {
this._resetCompilation();
...
}
...
}
...複製代碼
能夠看到,在 _resetCompilation
方法內部把 call
方法的值重置成了 _call
方法,這是由於咱們執行 call
方法時執行的是編譯好的靜態腳本,因此若是註冊事件回調時不重置成 _call
方法,那麼由於惰性函數的緣故,執行的靜態腳本就不會包含當前註冊的事件回調了。
咱們屢次提到了編譯生成的靜態腳本,那它究竟是如何編譯?又長什麼樣呢?爲了揭開這個神祕面紗,讓咱們從新回到 SyncHook
中的 compile
方法中去,它內部使用到了 factory
變量,這個變量是實例化 SyncHookCodeFactory
類的結果,而 SyncHookCodeFactory
類繼承自 HookCodeFactory
類。
// tapable/lib/SyncHook.js
...
class SyncHookCodeFactory extends HookCodeFactory {
content({ onError, onDone, rethrowIfPossible }) {
return this.callTapsSeries({
onError: (i, err) => onError(err),
onDone,
rethrowIfPossible
});
}
}
const factory = new SyncHookCodeFactory();
class SyncHook extends Hook {
...
compile(options) {
factory.setup(this, options);
return factory.create(options);
}
}
...複製代碼
接下來咱們來看下 SyncHookCodeFactory
是如何被實例化的,SyncHookCodeFactory
自己並無構造函數,咱們向上查看它的父類 HookCodeFactory
。HookCodeFactory
類的構造函數就是聲明瞭一些屬性,並無什麼特殊之處,另外 config
在目前版本的 Tapable
代碼中並無用上,因此不用管它。
// tapable/lib/HookCodeFactory.js
class HookCodeFactory {
constructor(config) {
this.config = config;
this.options = undefined;
this._args = undefined;
}
}
...複製代碼
咱們繼續看 compile
方法,它內部調用了 factory
的 setup
方法和 create
方法,這兩個方法都在 HookCodeFactory
類中。咱們先看 setup
方法,在調用時,instance
接收的是 SyncHook
的實例,options
接收的是 Hook
類中 _createCell
方法中傳入的對象。
class HookCodeFactory {
...
// factory.setup(this, options);
// 註釋中的 this 都是 SyncHook 的實例
// instance = this
// options = {
// taps: this.taps,
// interceptors: this.interceptors,
// args: this._args,
// type: type
// }
setup(instance, options) {
instance._x = options.taps.map(t => t.fn);
}
...
}複製代碼
setup
方法內部的邏輯是把 taps
數組中的配置對象轉換成只包含事件回調的數組並返回給 SyncHook
實例的 _x
屬性,這個 _x
屬性會在靜態腳本內部派上大用場,咱們以後再看。
接下來,咱們來看 create
方法,compile
方法最終返回的就是這個方法執行後結果。咱們最主要關心的是使用 new Function
來建立函數的這一段邏輯,這正是 Tapable
的核心所在,也就是它生成了靜態腳本。
class HookCodeFactory {
...
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;
...
}
// 清除初始化的操做
this.deinit();
return fn;
}
init(options) {
this.options = options;
// 複製一份事件回調參數聲明數組
this._args = options.args.slice();
}
deinit() {
this.options = undefined;
this._args = undefined;
}
...
}複製代碼
new Function
的第一個參數是函數須要的形參,這個形參接收是用 ,
分隔的字符串,可是實例化 SyncHook
類時傳入的參數聲明是數組類型,所以經過 args
方法拼接成字符串;args
方法接收一個對象,對象中有 before
和 after
,before
主要用於拼接 context
,after
主要用於拼接回調函數(例如 callAsync
傳入的回調函數)。
class HookCodeFactory {
...
args({ before, after } = {}) {
let allArgs = this._args;
if (before) allArgs = [before].concat(allArgs);
if (after) allArgs = allArgs.concat(after);
if (allArgs.length === 0) {
return "";
} else {
return allArgs.join(", ");
}
}
...
}複製代碼
new Function
的第二個參數即是函數體了,它是由 header
方法和 content
方法執行的結果拼接而成,咱們先看 header
方法,它內部就是聲明瞭一些以後須要用到變量,好比 _context
就是存儲 context
的對象,固然 _context
是對象仍是 undefined
,取決於 taps
的配置對象是否啓用了 context
,啓用那麼 _context
就是對象了。
另外,_x
、_taps
和 _interceptors
的值實際上都是 SyncHook
類的實例上對應的屬性。這邊的 this
由於 new Function
生成的函數最終是賦值給 SyncHook
類的實例的 call
方法,因此是指向 SyncHook
類的實例的。
class HookCodeFactory {
...
header() {
let code = "";
if (this.needContext()) {
code += "var _context = {};\n";
} else {
code += "var _context;\n";
}
code += "var _x = this._x;\n";
if (this.options.interceptors.length > 0) {
code += "var _taps = this.taps;\n";
code += "var _interceptors = this.interceptors;\n";
}
for (let i = 0; i < this.options.interceptors.length; i++) {
const interceptor = this.options.interceptors[i];
if (interceptor.call) {
code += `${this.getInterceptor(i)}.call(${this.args({ before: interceptor.context ? "_context" : undefined })});\n`;
}
}
return code;
}
needContext() {
// 找到一個配置對象啓用了 context 就返回 true
for (const tap of this.options.taps) if (tap.context) return true;
return false;
}
...
}複製代碼
最後就是 content
方法了,這個方法並不在 HookCodeFactory
類中,所以咱們前往繼承它的子類,也就是 SyncHookCodeFactory
類中查看。
// tapable/lib/SyncHookCodeFactory.js
...
class SyncHookCodeFactory extends HookCodeFactory {
// {
// onError: err => `throw ${err};\n`,
// onResult: result => `return ${result};\n`,
// resultReturns: true,
// onDone: () => "",
// rethrowIfPossible: true
// }
content({ onError, onDone, rethrowIfPossible }) {
return this.callTapsSeries({
onError: (i, err) => onError(err),
onDone,
rethrowIfPossible
});
}
}
...複製代碼
從上面代碼中能夠看到 content
方法內部調用了 HookCodeFactory
類的 callTapsSeries
方法,咱們須要繼續返回到 HookCodeFactory
類中。這邊有點繞的緣故在於:不一樣的鉤子它們的拼接邏輯是不同的,所以須要在子類中定義 content
方法,讓子類本身去寫拼接的邏輯。
下面是 callTapsSeries
方法的主體邏輯,其餘跟 SyncHook
不相關的代碼我給去掉了。
// tapable/lib/HookCodeFactory.js
class HookCodeFactory {
...
// {
// onDone: () => ""
// }
callTapsSeries({
onDone,
}) {
if (this.options.taps.length === 0) return onDone();
let code = "";
let current = onDone;
// 從後向前遍歷
for (let j = this.options.taps.length - 1; j >= 0; j--) {
const i = j;
// current 第一次的值是傳入的 onDone 函數,以後每次都是上一次拼接結果的箭頭函數,
// 這樣可以保證總體的事件回調是按照順序拼接的。
const done = current;
const content = this.callTap(i, {
onDone: done,
});
current = () => content;
}
code += current();
return code;
}
...
}
...複製代碼
每一個事件回調的調用代碼都是經過 callTap
方法拼接的,下面是它的代碼:
class HookCodeFactory {
...
callTap(tapIndex, { onDone }) {
let code = "";
code += `var _fn${tapIndex} = ${this.getTapFn(tapIndex)};\n`;
const tap = this.options.taps[tapIndex];
switch (tap.type) {
case "sync":
// 拼接調用代碼
code += `_fn${tapIndex}(${this.args({ before: tap.context ? "_context" : undefined })});\n`;
if (onDone) {
code += onDone();
}
break;
}
return code;
}
...
}複製代碼
在全部的事件回調都遍歷以後,callTapsSeries
方法中的 current
變量的值會相似下面這樣,current
執行以後就會獲得執行事件回調的腳本了。
// 3.最終 current 的值
current = () => {
var code = ` var _fn0 = _x[0]; _fn0(name); `
// 2.第二次循環 current 的值
code += () => {
var code = ` var _fn1 = _x[1]; _fn1(name); `;
// 1.第一次循環 current 的值
code += () => {
var code = ` var _fn2 = _x[2]; _fn2(name); `;
return code;
};
return code;
};
return code;
}複製代碼
到此爲止,函數體解析完畢,這也就表明着靜態腳本已經所有拼接,下面是拼接好的一份靜態腳本示例:
// 靜態腳本
(function anonymous(name) {
"use strict";
var _context;
var _x = this._x;
var _fn0 = _x[0];
_fn0(name);
var _fn1 = _x[1];
_fn1(name);
var _fn2 = _x[2];
_fn2(name);
})複製代碼
剩下的事,即是把 new Function
生成的函數返回給 call
方法,而後讓 call
方法去執行靜態腳本了。咱們經過流程圖來回顧一下建立靜態腳本的過程:
實際上我只是介紹了 Tapable
大部分主要的使用方式,像 MultiHook
之類的鉤子或者方法並無說明,這是由於文章的主要內容仍是在於鉤子類的使用以及下半部分原理解析相關,所以我只須要把它們涉及到的 API 和概念講解清楚即可,詳細的使用方式能夠看 Tapable
Github 的 README.md
。
原理解析部分我把 SyncHook
鉤子類做爲入口,一步步深刻解析了整個註冊和觸發流程,最讓人印象深入的即是 Tapable
的編譯生成靜態腳本了。在大致流程瞭解以後,若是想要了解其餘鉤子類,我建議能夠先調試把靜態腳本取出來,看看它生成的是什麼腳本,這樣反推會更容易理解代碼。