tapable是webpack插件机制的核心,它的作者就是webpack的作者Tobias Koppers。tapable的使用我就不再赘述了,官网上有完善的资料,我这里重点从源码的角度分析一下tapable是如何实现的。
基本流程
我们首先通过一段SyncHook
的示例代码来看看tapable最基本的执行流程是什么样子的。先上示例代码:
// debug/tapable-debug.js
const {
SyncHook,
} = require("tapable");
class Test {
constructor() {
this.hooks = {
abc: new SyncHook(['param1', 'param2']),
};
}
tap() {
this.hooks.abc.tap('tap1', function (param1, param2) {
console.log('tap1', param1, param2);
});
this.hooks.abc.tap('tap2', function (param1, param2) {
console.log('tap2', param1, param2);
});
this.hooks.abc.tap('tap3', function (param1, param2) {
console.log('tap3', param1, param2);
});
return this;
}
call() {
let res = this.hooks.abc.call('value1', 'value2');
console.log('res', res);
return this;
}
}
const test = new Test();
test.tap().call();
这段代码的执行结果很简单,如下所示:
// tap1 value1 value2
// tap2 value1 value2
// tap3 value1 value2
整个流程可以分为三步:
第一步:实例化SyncHook
对象(new SyncHook(['param1', 'param2'])
)
第二步:调用tap
挂载钩子函数
第三步:调用call
触发钩子函数
我们一步一步来看,首先来看第一步:实例化SyncHook
对象。
第一步:实例化SyncHook
对象
// node_modules/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 {
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
的构造函数都做了些什么(留意一下上面的SyncHookCodeFactory
部分,我们一会儿再回过头来看这部分的代码)。
// node_modules/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;
}
...
}
Hook
的构造函数里我们先重点关注四个信息,分别是:
-
this._args
:保存形参数组,在这里就是['param1', 'param2']
-
this.taps
:保存接下来通过test.tap()
挂载的钩子函数的相关信息 -
this.call
:这里会保存最后执行的Boss函数(划~重~点~) -
this._x
:这里会保存通过tap
挂载的钩子函数的函数本体
第一步构造函数到这里就结束了,接下来我们来看第二步:调用tap
挂载钩子函数。
第二步:调用tap
挂载钩子函数
// node_modules/tapable/lib/Hook.js
class Hook {
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 = 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);
}
_insert(item) {
this._resetCompilation();
let before;
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];
this.taps[i + 1] = x;
const xStage = x.stage || 0;
if (before) {
if (before.has(x.name)) {
before.delete(x.name);
continue;
}
if (before.size > 0) {
continue;
}
}
if (xStage > stage) {
continue;
}
i++;
break;
}
this.taps[i] = item;
}
}
无关代码有点多,我们目前只需要关注下面精简后的部分就可以了:
// node_modules/tapable/lib/Hook.js
class Hook {
tap(options, fn) {
if (typeof options === "string") options = { name: options };
options = Object.assign({ type: "sync", fn: fn }, options);
this._insert(options);
}
...
_insert(item) {
this.taps[i] = item;
}
}
是不是感觉一下子清晰多了?可以看到tap
做的事情其实也很简单,就是把钩子函数的相关信息options
保存到this.taps
数组里头。所以当test.tap()
调用完毕后,this.hooks.abc.taps
中的信息就是这样的:
接下来就是我们的重头戏了,第三步:调用call
触发钩子函数。
第三步:调用call
触发钩子函数
我们首先要问一个问题,这个call
函数是从哪里来的呢?还记得Hook
的构造函数嘛~
// node_modules/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;
}
}
function createCompileDelegate(name, type) {
return function lazyCompileHook(...args) {
this[name] = this._createCall(type); // 此时的name就是"call"
return this[name](...args); // 此时的args就是['value1', 'value2']
};
}
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
}
});
找到了!原来在Hook.prototype
上已经定义好了_call
函数,而这个_call
函数的值就是createCompileDelegate
返回的lazyCompileHook
函数,在Hook
的构造函数中就把this._call
赋值给了this.call
。那么当我们执行this.call
函数的时候,实际上就走到了lazyCompileHook
函数里头。从lazyCompileHook
函数的代码来看,this.call
被赋值为this._createCall(type)
的返回值(注意!它就是我们的终极Boss函数),然后通过this[name](...args)
立即执行该函数。
这里我们先不着急深入this._createCall(type)
函数,我们先看看终极Boss函数长啥样儿再说:
// 哒~哒~哒~哒~
function anonymous(param1, param2) {
'use strict';
var _context;
var _x = this._x;
var _fn0 = _x[0];
_fn0(param1, param2);
var _fn1 = _x[1];
_fn1(param1, param2);
var _fn2 = _x[2];
_fn2(param1, param2);
}
这里的_fn0、_fn1、_fn2
就是我们通过tap
挂载的三个钩子函数,而param1
和param2
的值就是我们通过this.hooks.abc.call('value1', 'value2')
传入的'value1'
和'value2'
,再结合我们的示例代码一起看的话有没有一种豁然开朗的感觉呢:)
好的!接下来我们就深入this._createCall(type)
函数来看看这个终极Boss函数是如何生成的吧~
// node_modules/tapable/lib/Hook.js
class Hook {
compile(options) {
throw new Error("Abstract: should be overriden");
}
_createCall(type) {
return this.compile({
taps: this.taps,
interceptors: this.interceptors,
args: this._args,
type: type
});
}
}
可以看到this._createCall(type)
调用了this.compile
,而Hook
中的compile
函数是直接抛错的,从错误信息我们可以看到作者提示了我们这个compile
函数是需要被继承Hook
的子类复写的,在我们这里就是SyncHook
,所以我们继续走到SyncHook
中去看看它的compile
函数是如何实现的:
// node_modules/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 {
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
的compile
函数中的第一行代码是调用factory
也就是SyncHookCodeFactory
的setup
方法,而上面的SyncHookCodeFactory
继承HookCodeFactory
时只复写了一个content
函数,那么我们自然能想到这个setup
方法应该是位于HookCodeFactory
里面的。事实就是如此,继续往里走,看看下面的setup
方法,这里的instance
就是我们的this.hooks.abc
,所以你就找到了我们的钩子函数是在哪儿赋值给this._x
的。继续回到compile
函数看看factory.create(options)
都干了些什么:
// node_modules/tapable/lib/HookCodeFactory.js
class HookCodeFactory {
constructor(config) {
this.config = config;
this.options = undefined;
this._args = undefined;
}
create(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;
}
return fn;
}
setup(instance, options) {
instance._x = options.taps.map(t => t.fn);
}
}
上面是精简过之后的代码,而factory.create(options)
返回的new Function(...)
就是我们一直心心念念的终极Boss函数(anonymous
)啦~~
其他部分我先略过,直接看this.content
函数,这个函数会回到我们的SyncHookCodeFactory
类中,然后通过调用HookCodeFactory
的callTapsSeries
函数来生成并返回我们的终极Boss函数的函数体字符串。
后面的部分我就不展开细讲了,因为只是一些根据Hook
的类型不同走不同的函数体字符串拼接逻辑罢了,这部分内容说也很难说清楚,所以还是有赖于读者自己debug进去走一走看一看,整个过程就会比较清楚了。
特大喜讯!特大喜讯!
告诉读者朋友们一个特大好消息!你只要把上面讲的SyncHook
的整个流程搞清楚了,那么下面的tapable
的所有Hook
的流程你就都搞清楚了!
const {
SyncHook,
SyncBailHook,
SyncWaterfallHook,
SyncLoopHook,
AsyncParallelHook,
AsyncParallelBailHook,
AsyncSeriesHook,
AsyncSeriesBailHook,
AsyncSeriesWaterfallHook,
AsyncSeriesLoopHook,
} = require("tapable");
因为,其实其他的Hook
的代码跟SyncHook
的结构是一模一样的,包括构造函数和tap
挂载钩子函数的部分都一样(因为实际上走的都是共同的父类Hook
的逻辑),只有各自的HookCodeFactory
的content
函数部分的实现不一样罢了,不信的话你看看SyncHook
跟SyncBailHook
的代码对比:
再看看
SyncHook
跟AsyncParallelHook
的代码对比:所以,有木有一种一箭N雕的中了大奖的感觉呢:)
写在最后
到这里,tapable
的基本流程就讲完了。我们现在知道了原来tapable
在调用call
的时候会通过new Function(...)
来实时生成一个函数执行我们挂载的钩子函数,也知道了不同的Hook
之间其实主要就是生成终极Boss函数的HookCodeFactory.content
函数的实现有差异罢了。其实,tapable
还提供了共享上下文context
对象、以及为所有的Hook
都提供了intercept
拦截器的功能,不过这块内容就交给聪明的读者你自己去👀啦(小小的透露一下,差别其实也还是在HookCodeFactory.content
函数里面哦)。