Webpack 源码阅读之 Tapable
webpack 采用 Tapable 来进行流程控制,在这套体系上,内部近百个插件有条不紊,还能支持外部开发自定义插件来扩展功能,所以在阅读 webpack 源码前先了解 Tapable 的机制是很有必要的。
Tapable 的基本使用方法就不介绍了,可以参考官方文档
- https://github.com/webpack/ta…
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'; })