Webpack源码阅读之Tapable
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'; })