本文是tapable项目源码阅读的第一篇,文章的目的是大致梳理一下tapable源码的结构,为接下来深入细节铺平道理,打好基础。
文章假设读者对tapable的用法有基本的了解,tapable文档在这里
整体感知
话不多说,直接上项目结构图:
-tapable
|-lib
|-index.js
|-Hook.js
|-SyncHook.js
|-SyncBailHook.js
|-SyncLoopHook.js
|-SyncWaterfallHook.js
|-AsyncParallelBailHook.js
|-AsyncParallelHook.js
|-AsyncSeriesBailHook.js
|-AsyncSeriesHook.js
|-AsyncSeriesLoopHook.js
|-AsyncSeriesWaterfallHook.js
|-HookCodeFactory.js
|-HookMap.js
|-MultiHook.js
|-util-browser.js
|-package.json
|-__tests__
|- 测试用例
项目的结构非常简单,可以看到tapable支持的所有hook类型以及两个辅助类型都以单文件的形式存在。
通过package.json
的main
字段找到项目的入口index.js
// index.js
exports.SyncHook = require("./SyncHook");
exports.SyncBailHook = require("./SyncBailHook");
// ...
入口内容很简单,仅仅是将所有的引入并导出了tapable支持的10种hook类型,以及两个辅助类。
Hook的实例方法探究
可以提前剧透的一点是,tapable
导出的10种hook类型,其实例化的流程是基本一致的,所以接下来,我们选定最简单的SyncHook
类型来探究项目的大体结构。
看代码之前,我们思考一个问题:我们希望在SyncHook
的代码里面看到什么?
根据SyncHook
的用法,我想大概有以下几点:
- 插件注册的
tap
方法 - 执行所有已注册插件的
call
方法 - hook的拦截器注册
intercept
方法
带着期待,我们进入SyncHook.js
function SyncHook(args = [], name = undefined) {
const hook = new Hook(args, name);
hook.constructor = SyncHook;
hook.tapAsync = TAP_ASYNC;
hook.tapPromise = TAP_PROMISE;
hook.compile = COMPILE;
return hook;
}
SyncHook.prototype = null;
SyncHook子类利用实例继承的方式,继承了Hook父类,并将constructor指向自身,重写了tapAsync
,tapPromise
和compile
方法。对于SyncHook类型的钩子,tapAsync
和tapPromise
方法调用都会直接抛出错误。compile
方法,我们暂时还不知道有啥用。
我们期待中的tap
,call
和intercept
方法并没有出现,所以,接着去基类Hook.js
文件去找。
Hook类结构如下:
class Hook {
constructor(args = [], name = undefined) {
// ...
this.taps = [];
this.interceptors = [];
this.call = CALL_DELEGATE;
this.callAsync = CALL_ASYNC_DELEGATE;
this.promise = PROMISE_DELEGATE;
this.compile = this.compile;
this.tap = this.tap;
this.tapAsync = this.tapAsync;
this.tapPromise = this.tapPromise;
}
intercept() { /**/ }
tap() { /**/ }
tapAsync() { /**/ }
tapPromise() { /**/ }
}
果然,我们期待的所有方法,都有在这里定义,我们逐一来看。
插件注册方法:tap
首先来看插件注册的tap方法(注意tap是SyncHook类的注册方法,对于其他类型的钩子,还有其他的注册方式)。
class Hook {
// ...
tap(options, fn) {
this._tap("sync", options, fn);
}
_tap(type, options, fn) {
// 省略标准化options的代码,标准化之后options包含了type和fn信息
this._insert(options);
}
tapAsync(options, fn) {
this._tap("async", options, fn);
}
tapPromise(options, fn) {
this._tap("promise", options, fn);
}
_insert(item) {
// 省略寻找插入位置的逻辑代码
this.taps[i] = item;
}
}
从这里可以看出,对于一个hook实例,其插件注册的本质,其实是将我们传入的参数标准化之后,推入实例的taps数组中。并且,taps是一个有序的列表,可以推测这和插件将来执行的顺序是有关系的(排序的标准将在未来的文章中详细讲解)。
以上讲的是SyncHook的插件注册方式,那其他类型的插件呢?
在tapable中,插件的注册方式被分为了三大类(有的钩子类型支持多种注册方式),sync
,async
,promise
,对应的插件注册方法分别是tap
,tapAsync
,tapPromise
,这三者的注册方式完全一样,都是调用了this._tap
将插件配置插入到taps数组中。
钩子拦截器方法:intercept
class Hook {
// ...
intercept(interceptor) {
this._resetCompilation();
this.interceptors.push(Object.assign({}, interceptor));
if (interceptor.register) {
// 中间件注册时,对所有**已经**注册的插件执行register函数
for (let i = 0; i < this.taps.length; i++) {
this.taps[i] = interceptor.register(this.taps[i]);
}
}
}
}
插件注册的代码也很简单,其本质是将intercept方法接收到的参数直接推入实例属性interceptors数组中。同时遍历已经注册的插件,以插件的配置为参数,执行当前拦截器的register方法。(interceptor配置还有很多其他方法,比如,call, tap, loop等等,它们将在合适的时候执行)
插件列表的调用方法:call
首先注意,call是SyncHook类型的插件调用方法,还有其他的调用方式(callAsync,promise)
相对插件注册和拦截器注册,插件调用方法要复杂的多。
回到Hook.js
:
const CALL_DELEGATE = function(...args) {
this.call = this._createCall("sync");
return this.call(...args);
};
class Hook {
constructor(args = [], name = undefined) {
// ... 省略其他代码
this.call = CALL_DELEGATE;
}
// 根据提供的类型,生成对应的插件执行方式
_createCall(type) {
return this.compile({
// ...
});
}
compile(options) {
throw new Error("Abstract: should be overridden");
}
}
call方法的执行被代理到CALL_DELEGATE
函数,实际执行逻辑的call方法是由this._createCall
生成,而该方法直接返回了this.compile
方法的执行结果,所以可以总结为,插件调用的逻辑是由钩子实例的compile方法生成。而这里的compile方法是一个抽象方法,需要在具体的钩子里面去实现。
我们正在以SyncHook为例,所以,进入到SyncHook.js
,找到compile方法:
const factory = new SyncHookCodeFactory();
const COMPILE = function(options) {
factory.setup(this, options);
return factory.create(options);
}
function SyncHook(args = [], name = undefined) {
const hook = new Hook(args, name);
// ...
// compile方法生成调用hook插件的call/callAsync/promise方法
hook.compile = COMPILE;
return hook;
}
class SyncHookCodeFactory extends HookCodeFactory {
content({ onError, onDone, rethrowIfPossible }) {
return this.callTapsSeries({
onError: (i, err) => onError(err),
onDone,
rethrowIfPossible
});
}
}
这里的compile方法也是由SyncHookCodeFactory
实例的create
方法生成,该类继承于HookCodeFactory
,并定义了content方法。没找到create,继续去HookCodeFactory
找:
class HookCodeFactory {
// ...
// 生成compile函数,compile函数返回的是hook的调用方法,比如synchook的call方法
create(options) {
// ...
let fn;
switch (this.options.type) {
case "sync":
fn = new Function(
this.args(),
'"use strict";\n' +
this.header() + // 参数定义
this.contentWithInterceptors({
// ...
})
);
break;
case "async":
// ...
case "promise":
// ...
}
// ...
return fn;
}
setup(instance, options) {
instance._x = options.taps.map(t => t.fn);
}
contentWithInterceptors(options) {
if (this.options.interceptors.length > 0) {
// ...
let code = "";
for (let i = 0; i < this.options.interceptors.length; i++) {
const interceptor = this.options.interceptors[i];
if (interceptor.call) {
// 执行所有的中间件的call函数
code += `${this.getInterceptor(i)}.call(${this.args({
before: interceptor.context ? "_context" : undefined
})});\n`;
}
}
code += this.content(
Object.assign(options, { /**/ })
);
return code;
} else {
return this.content(options);
}
}
前面说到,代码工厂类的create方法生成了钩子实例的compile方法(compile方法执行生成插件调用的方法call)。观察create方法代码,它根据传入钩子类型,利用Function构造函数实例化出一个函数并返回,这个返回的函数就是钩子实例的compile方法。
进一步看这个函数的实例化过程,主要的逻辑都在this.contentWithInterceptors
里面,该方法先检查有没有拦截器,如果有拦截器,则在添加插件执行代码之前,先加上拦截器call方法的执行逻辑。插件逻辑代码则由this.content
添加,此方法在特定类型的工厂类型中定义,对于SyncHook类钩子就是SyncHookCodeFactory
工厂类。
有点绕,我们试着捋一捋:
SyncHook类型的钩子,在实例化之后,并没有调度插件逻辑的方法,虽然有一个call,但这是一个傀儡函数,执行这个函数时,才会调用compile编译出一个真正的调度函数call,然后执行。(啥?Hook.js的_createCall
方法是干啥的?如果你试着删掉_createCall
直接调用this.compile也行,不过三个代理函数是不是多了三处重复的代码?所以理解源码逻辑时,剥掉这一层会少绕一点弯子)
compile的本质是生成了调度插件的函数,而compile直接返回了create的执行结果,所以create方法的本质也是生成了调度插件的函数。
create方法定义在工厂类型的基类上,该方法通过Function构造函数,逐行生成插件调度方法的逻辑代码,其主要的调度逻辑代码由工厂类方法contentWithInterceptors
生成
contentWithInterceptors
则通过工厂实例方法content
生成插件逻辑调度的代码,同时,它需要处理拦截器,在添加插件逻辑之前,它遍历了所有的拦截器,并添加了interceptor.call
方法的执行逻辑。
把这个逻辑翻过来(其中factory是工厂类实例):
factory.content(生成插件调度主体代码的字符串) -> factory.contentWithInterceptors(在插件调度代码之前添加拦截器执行代码字符串) -> factory.create(添加函数变量定义并实例化函数) -> hook.compile(设置工厂配置,调用create生成并返回调度函数) -> hook.call
以上即是整个tapable项目的整体结构的初步感知,细节内容,敬请期待。