Webpack源碼閱讀之Tapable
webpack採用Tapable來進行流程控制,在這套體系上,內部近百個插件有條不紊,還能支持外部開發自定義插件來擴展功能,所以在閱讀webpack源碼前先了解Tapable的機制是很有必要的。
Tapable的基本使用方法就不介紹了,可以參考官方文檔
1. 例子
從網上拷貝了一個簡單的使用例子:
//main.js
const { SyncHook } = require('tapable')
//創建一個簡單的同步串行鉤子
let h1 = new SyncHook(['arg1,arg2']);
//在鉤子上添加訂閱者,鉤子被call時會觸發訂閱的回調函數
h1.tap('A',function(arg){
console.log('A',arg);
return 'b'
})
h1.tap('B',function(){
console.log('b')
})
h1.tap('C',function(){
console.log('c')
})
//在鉤子上添加攔截器
h1.intercept({
//鉤子被call的時候觸發
call: (...args)=>{
console.log(...args, '-------------intercept call');
},
//定義攔截器的時候註冊taps
register:(tap)=>{
console.log(tap, '------------------intercept register');
},
//循環方法
loop:(...args)=>{
console.log(...args, '---------------intercept loop')
},
//tap調用前觸發
tap:(tap)=>{
console.log(tap, '---------------intercept tap')
}
})
//觸發鉤子
h1.call(6)
2. 調試方法
最直接的方式是在 chrome 中通過斷點在關鍵代碼上進行調試,在如何使用 Chrome 調試webpack源碼中學到了調試的技巧:
我們可以用
node-inspector
在chrome中調試nodejs代碼,這比命令行中調試方便太多了。nodejs 從 v6.x 開始已經內置了一個 inspector,當我們啓動的時候可以加上--inspect
參數即可:node --inspect app.js
然後打開chrome,打開一個新頁面,地址是:
chrome://inspect
,就可以在 chrome 中調試你的代碼了。如果你的JS代碼是執行一遍就結束了,可能沒時間加斷點,那麼你可能希望在啓動的時候自動在第一行自動加上斷點,可以使用這個參數
--inspect-brk
,這樣會自動斷點在你的第一行代碼上。
3. 源碼分析
安裝好Tapable包,根據上述方法,我們運行如下命令:
node --inspect-brk main.js
3.1 初始化
在構造函數處打上斷點,step into可以看到SyncHook繼承自Hook,上面定義了一個compile函數。
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);
}
}
再step into來到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;
}
...
}
h1初始化完成:
h1:{
call: ƒ lazyCompileHook(...args)
callAsync: ƒ lazyCompileHook(...args)
interceptors: []
promise: ƒ lazyCompileHook(...args)
taps: []
_args: ["options"]
_x: undefined
}
3.2 註冊觀察者
Tapable採用觀察者模式來進行流程管理,在鉤子上使用tap方法註冊觀察者,鉤子被call時,觀察者對象上定義的回調函數按照不同規則觸發(鉤子類型不同,觸發順序不同)。
Step into tap方法:
//options='A', fn=f(arg)
tap(options, fn) {
//類型檢測
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 ==>{type: "sync", fn: fn,name:options}
options = Object.assign({ type: "sync", fn: fn }, options);
if (typeof options.name !== "string" || options.name === "")
throw new Error("Missing name for tap");
//這裏調用攔截器上的register方法,當intercept定義在tap前時,會在這裏調用intercept.register(options), 當intercept定義在tap後時,會在intercept方法中調用intercept.register(this.taps)
options = this._runRegisterInterceptors(options);
//根據before, stage 的值來排序this.taps = [{type: "sync", fn: fn,name:options}]
this._insert(options);
}
當三個觀察者註冊完成後,h1變爲:
{
call: ƒ lazyCompileHook(...args)
callAsync: ƒ lazyCompileHook(...args)
interceptors: []
promise: ƒ lazyCompileHook(...args)
taps:[
0: {type: "sync", fn: ƒ, name: "A"}
1: {type: "sync", fn: ƒ, name: "B"}
2: {type: "sync", fn: ƒ, name: "C"}
]
length: 3
__proto__: Array(0)
_args: ["options"]
_x: undefined
}
3.3 註冊攔截器
在調用h1.intercept() 處step into,可以看到定義的攔截回調被推入this.interceptors中。
intercept(interceptor) {
this._resetCompilation();
this.interceptors.push(Object.assign({}, interceptor));
if (interceptor.register) {
for (let i = 0; i < this.taps.length; i++)
this.taps[i] = interceptor.register(this.taps[i]);
}
}
此時h1
變爲:
{
call: ƒ lazyCompileHook(...args)
callAsync: ƒ lazyCompileHook(...args)
interceptors: Array(1)
0:
call: (...args) => {…}
loop: (...args) => {…}
register: (tap) => {…}
tap: (tap) => {…}
__proto__: Object
length: 1
__proto__: Array(0)
promise: ƒ lazyCompileHook(...args)
taps: Array(3)
0: {type: "sync", fn: ƒ, name: "A"}
1: {type: "sync", fn: ƒ, name: "B"}
2: {type: "sync", fn: ƒ, name: "C"}
length: 3
__proto__: Array(0)
_args: ["options"]
_x: undefined
}
3.4 鉤子調用
在觀察者和攔截器都註冊後,會保存在this.interceptors
和this.taps
中;當我們調用h1.call()
函數後,會按照一定的順序調用它們,現在我們來看看具體的流程,在call方法調用時step into, 會來到Hook.js中的createCompileDelegate函數。
function createCompileDelegate(name, type) {
return function lazyCompileHook(...args) {
this[name] = this._createCall(type);
return this[name](...args);
};
}
因爲_call函數定義在Hook原型上,並通過在構造函數中this.call=this.__call
賦值。
Object.defineProperties(Hook.prototype, {
_call: {
value: createCompileDelegate("call", "sync"),
configurable: true,
writable: true
},
_promise: {
value: createCompileDelegate("promise", "promise"),
configurable: true,
writable: true
},
_callAsync: {
value: createCompileDelegate("callAsync", "async"),
configurable: true,
writable: true
}
});
按照執行順序轉到 this._createCall
:
_createCall(type) {
return this.compile({
taps: this.taps,
interceptors: this.interceptors,
args: this._args,
type: type
});
}
在this.compile()
處step into 跳轉到SyncHook.js上的compile方法上,其實我們在Hook.js上就可以看到,compile是需要在子類上重寫的方法, 在SyncHook上其實現如下:
compile(options) {
factory.setup(this, options);
return factory.create(options);
}
class SyncHookCodeFactory extends HookCodeFactory {
content({ onError, onDone, rethrowIfPossible }) {
return this.callTapsSeries({
onError: (i, err) => onError(err),
onDone,
rethrowIfPossible
});
}
}
const factory = new SyncHookCodeFactory();
在factory.setup
處step into,可以看到factory.setup(this, options)
其實只是把taps上註冊的回調推入this._x
:
setup(instance, options) {
instance._x = options.taps.map(t => t.fn);
}
在factory.create
中定義了this.interceptors
和this.taps
的具體執行順序,在這裏step into:
//HookFactory.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;
case "async":
....
case "promise":
....
}
this.deinit();
return fn;
}
可以看到這裏是通過new Function構造函數傳入this.interceptors
和this.taps
動態進行字符串拼接生成函數體執行的。
在this.header()
中打斷點:
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;
}
生成的code如下,其執行了攔截器中定義的call回調:
"var _context;
var _x = this._x;
var _taps = this.taps;
var _interceptors = this.interceptors;
_interceptors[0].call(options);
在this.content()
打斷點,可以看到this.content定義在HookCodeFactory
中:
class SyncHookCodeFactory extends HookCodeFactory {
content({ onError, onDone, rethrowIfPossible }) {
return this.callTapsSeries({
onError: (i, err) => onError(err),
onDone,
rethrowIfPossible
});
}
}
其返回了定義在子類中的callTapsSeries
方法:
callTapsSeries({
onError,
onResult,
resultReturns,
onDone,
doneReturns,
rethrowIfPossible
}) {
if (this.options.taps.length === 0) return onDone();
const firstAsync = this.options.taps.findIndex(t => t.type !== "sync");
const somethingReturns = resultReturns || doneReturns || false;
let code = "";
let current = onDone;
for (let j = this.options.taps.length - 1; j >= 0; j--) {
const i = j;
const unroll = current !== onDone && this.options.taps[i].type !== "sync";
if (unroll) {
code += `function _next${i}() {\n`;
code += current();
code += `}\n`;
current = () => `${somethingReturns ? "return " : ""}_next${i}();\n`;
}
const done = current;
const doneBreak = skipDone => {
if (skipDone) return "";
return onDone();
};
const content = this.callTap(i, {
onError: error => onError(i, error, done, doneBreak),
onResult:
onResult &&
(result => {
return onResult(i, result, done, doneBreak);
}),
onDone: !onResult && done,
rethrowIfPossible:
rethrowIfPossible && (firstAsync < 0 || i < firstAsync)
});
current = () => content;
}
code += current();
return code;
}
具體的拼接步驟這裏就不詳述了,感興趣可以自己debugger,嘿嘿。最後返回的code爲:
var _tap0 = _taps[0];
_interceptors[0].tap(_tap0);
var _fn0 = _x[0];
_fn0(options);
var _tap1 = _taps[1];
_interceptors[0].tap(_tap1);
var _fn1 = _x[1];
_fn1(options);
var _tap2 = _taps[2];
_interceptors[0].tap(_tap2);
var _fn2 = _x[2];
_fn2(options);
var _tap3 = _taps[3];
_interceptors[0].tap(_tap3);
var _fn3 = _x[3];
_fn3(options);
這裏定義了taps和其相應的攔截器的執行順序。
4. webpack調試技巧
當我們調試webpack源碼是,經常需要在鉤子被call的代碼處調試到具體插件的執行過程,可以參考上述過程進行調試,具體步驟爲:
- 在call處step into
- 在return處step into
-
得到生成的動態函數
(function anonymous(options ) { "use strict"; var _context; var _x = this._x; var _taps = this.taps; var _interceptors = this.interceptors; _interceptors[0].call(options); var _tap0 = _taps[0]; _interceptors[0].tap(_tap0); var _fn0 = _x[0]; _fn0(options); var _tap1 = _taps[1]; _interceptors[0].tap(_tap1); var _fn1 = _x[1]; _fn1(options); var _tap2 = _taps[2]; _interceptors[0].tap(_tap2); var _fn2 = _x[2]; _fn2(options); var _tap3 = _taps[3]; _interceptors[0].tap(_tap3); var _fn3 = _x[3]; _fn3(options); })
- 在fn(options)處打step into
-
回到tap註冊的函數
h1.tap('A', function (arg) { console.log('A',arg); return 'b'; })