乐趣区

不满足于只会使用系列: tapable

全方位的, 零死角的, 分析 tapable 源码
上一遍博文中, 我们谈到了 tapable 的用法, 现在我们来深入一下 tap 究竟是怎么运行的, 怎么处理, 控制 tap 进去的钩子函数, 拦截器又是怎么运行的.
俺们先从同步函数说起, 异步就留给你们做练习把 (哈哈哈);
tap
这里有一个例子
let SyncHook = require(‘./lib/SyncHook.js’)

let h1 = new SyncHook([‘options’]);

h1.tap(‘A’, function (arg) {
console.log(‘A’,arg);
return ‘b’; // 除非你在拦截器上的 register 上调用这个函数, 不然这个返回值你拿不到.
})

h1.tap(‘B’, function () {
console.log(‘b’)
})
h1.tap(‘C’, function () {
console.log(‘c’)
})
h1.tap(‘D’, function () {
console.log(‘d’)
})

h1.intercept({
call: (…args) => {
console.log(…args, ‘————-intercept call’);
},
//
register: (tap) => {
console.log(tap, ‘——————intercept register’);

return tap;
},
loop: (…args) => {
console.log(…args, ‘————-intercept loop’)
},
tap: (tap) => {
console.log(tap, ‘——————-intercept tap’)

}
})
h1.call(6);
new SyncHook([‘synchook’])
首先先创建一个同步钩子对象, 那这一步会干什么呢?
这一步会先执行超类 Hook 的初始化工作
// 初始化
constructor(args) {
// 参数必须是数组
if (!Array.isArray(args)) args = [];
// 把数组参数赋值给 _args 内部属性, new 的时候传进来的一系列参数.
this._args = args;
// 绑定 taps, 应该是事件
this.taps = [];
// 拦截器数组
this.interceptors = [];
// 暴露出去用于调用同步钩子的函数
this.call = this._call;
// 暴露出去的用于调用异步 promise 函数
this.promise = this._promise;
// 暴露出去的用于调用异步钩子函数
this.callAsync = this._callAsync;
// 用于生存调用函数的时候, 保存钩子数组的变量, 现在暂时先不管.
this._x = undefined;
}
第二部 .tap()

现在我们来看看调用了 tap() 方法后发生了什么
tap(options, fn) {
// 下面是一些参数的限制, 第一个参数必须是字符串或者是带 name 属性的对象,
// 用于标明钩子, 并把钩子和名字都整合到 options 对象里面
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);
}
现在我们来看看如何注册拦截器
_runRegisterInterceptors(options) {
// 现在这个参数应该是这个样子的 {fn: function…, type: sync,name: ‘A’}
// 遍历拦截器, 有就应用, 没有就把配置返还回去
for (const interceptor of this.interceptors) {
if (interceptor.register) {
// 把选项传入拦截器注册, 从这里可以看出, 拦截器的 register 可以返回一个新的 options 选项, 并且替换掉原来的 options 选项, 也就是说可以在执行了一次 register 之后 改变你当初 tap 进去的方法
const newOptions = interceptor.register(options);
if (newOptions !== undefined) options = newOptions;
}
}
return options;
}
注意: 这里执行的 register 拦截器是有顺序问题的, 这个执行在 tap() 里面, 也就是说, 你这个拦截器要在调用 tap(), 之前就调用 intercept() 添加的.
那拦截器是怎么添加进去的呢, 来看下 intercept()
intercept(interceptor) {
// 重置所有的 调用 方法, 在教程中我们提到了 编译出来的调用方法依赖的其中一点就是 拦截器. 所有每添加一个拦截器都要重置一次调用方法, 在下一次编译的时候, 重新生成.
this._resetCompilation();
// 保存拦截器 而且是复制一份, 保留原本的引用
this.interceptors.push(Object.assign({}, interceptor));
// 运行所有的拦截器的 register 函数并且把 taps[i],(tap 对象) 传进去.
// 在 intercept 的时候也会遍历执行一次当前所有的 taps, 把他们作为参数调用拦截器的 register, 并且把返回的 tap 对象 (tap 对象就是指 tap 函数里面把 fn 和 name 这些信息整合起来的那个对象) 替换了原来的 tap 对象, 所以 register 最好返回一个 tap, 在例子中我返回了原来的 tap, 但是其实最好返回一个全新的 tap
if (interceptor.register) {
for (let i = 0; i < this.taps.length; i++)
this.taps[i] = interceptor.register(this.taps[i]);
}
}
注意: 也就是在调用 tap() 之后再传入的拦截器, 会在传入的时候就为每一个 tap 调用 register 方法
现在我们来看看_insert
_insert(item) {
// 重置资源, 因为每一个插件都会有一个新的 Compilation
this._resetCompilation();
// 顺序标记, 这里联合 __test__ 包里的 Hook.js 一起使用
// 看源码不懂, 可以看他的测试代码, 就知道他写的是什么目的.
// 从测试代码可以看到, 这个 {before} 是插件的名字.
let before;
// before 可以是单个字符串插件名称, 也可以是一个字符串数组插件.
if (typeof item.before === “string”) {
before = new Set([item.before]);
}
else if (Array.isArray(item.before)) {
before = new Set(item.before);
}
// 阶段
// 从测试代码可以知道这个也是一个控制顺序的属性, 值越小, 执行得就越在前面
// 而且优先级低于 before
let stage = 0;
if (typeof item.stage === “number”) stage = item.stage;
let i = this.taps.length;
// 遍历所有 `tap` 了的函数, 然后根据 stage 和 before 进行重新排序.
// 假设现在 tap 了 两个钩子 A B `B` 的配置是 {name: ‘B’, before: ‘A’}
while (i > 0) {// i = 1, taps = [A]
i–;// i = 0 首先 – 是因为要从最后一个开始
const x = this.taps[i];// x = A
this.taps[i + 1] = x;// i = 0, taps[1] = A i+1 把当前元素往后移位, 把位置让出来
const xStage = x.stage || 0;// xStage = 0
if (before) {// 如果有这个属性就会进入这个判断
if (before.has(x.name)) {// 如果 before 有 x.name 就会把这个插件名称从 before 这个列表里删除, 代表这个钩子位置已经在当前的钩子之前
before.delete(x.name);
continue;// 如果 before 还有元素, 继续循环, 执行上面的操作
}
if (before.size > 0) {
continue;// 如果 before 还有元素, 那就一直循环, 直到第一位.
}
}
if (xStage > stage) {// 如果 stage 比当前钩子的 stage 大, 继续往前挪
continue;
}
i++;
break;
}
this.taps[i] = item;// 把挪出来的位置插入传进来的钩子
}
这其实就是一个排序算法, 根据 before, stage 的值来排序, 也就是说你可以这样 tap 进来一个插件
h1.tap({
name: ‘B’,
before: ‘A’
}, () => {
console.log(‘i am B’)
})
发布订阅模式
发布订阅模式是一个在前后端都盛行的一个模式, 前端的 promise, 事件, 等等都基于发布订阅模式, 其实 tapable 也是一种发布订阅模式, 上面的 tap 只是订阅了钩子函数, 我们还需要发布他, 接下来我们谈谈 h1.call(), 跟紧了, 这里面才是重点.
我们可以在初始化中看到 this.call = 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
}
});
结果很明显, 这个函数是由 createCompileDelegate(), 这个函数返回的, 依赖于, 函数的名字以及钩子的类型.
createCompileDelegate(name, type)
function createCompileDelegate(name, type) {
return function lazyCompileHook(…args) {
// 子类调用时,this 默认绑定到子类
// (不明白的可以了解 js this 指向, 一个函数的 this 指向调用他的对象, 没有就是全局, 除非使用 call apply bind 等改变指向)
// 在我们的例子中, 这个 this 是 SyncHook
this[name] = this._createCall(type);
// 用 args 去调用 Call
return this[name](…args);
};
}
在上面的注释上可以加到, 他通过闭包保存了 name 跟 type 的值, 在我们这个例子中, 这里就是 this.call = this._createCall(‘sync’); 然后把我们外部调用 call(666) 时 传入的参数给到他编译生成的方法中.
注意, 在我们这个例子当中我在 call 的时候并没有传入参数.
这时候这个 call 方法的重点就在_createCall 方法里面了.
_createCall()
_createCall(type) {

// 传递一个整合了各个依赖条件的对象给子类的 compile 方法
return this.compile({
taps: this.taps,
interceptors: this.interceptors,
args: this._args,
type: type
});
}

从一开始, 我们就在 Hook.js 上分析, 我们来看看 Hook 上的 compile
compile(options) {
throw new Error(“Abstract: should be overriden”);
}
清晰明了, 这个方法一定要子类复写, 不然报错, 上面的_createCompileDelegate 的注释也写得很清楚, 在当前的上下文中,this 指向的是, 子类, 在我们这个例子中就是 SyncHook
来看看 SyncHook 的 compile
compile(options) {
// 现在 options 是由 Hook 里面 传到这里的
// options
// {
// taps: this.taps, tap 对象数组
// interceptors: this.interceptors, 拦截器数组
// args: this._args,
// type: type
// }
// 对应回教程中的编译出来的调用函数依赖于的那几项看看, 是不是这些, 钩子的个数,new SyncHook([‘arg’]) 的参数个数, 拦截器的个数, 钩子的类型.
factory.setup(this, options);

return factory.create(options);
}
好吧 现在来看看 setup, 咦? factory 怎么来的, 原来
const factory = new SyncHookCodeFactory();
是 new 出来的
现在来看看 SyncHookCodeFactory 的父类 HookCodeFactory
constructor(config) {

// 这个 config 作用暂定. 因为我看了这个文件, 没看到有引用的地方,
// 应该是其他子类有引用到
this.config = config;
// 这两个不难懂, 往下看就知道了
this.options = undefined;
this._args = undefined;
}
现在可以来看一下 setup 了
setup(instance, options) {
// 这里的 instance 是 syncHook 实例, 其实就是把 tap 进来的钩子数组给到钩子的_x 属性里.
instance._x = options.taps.map(t => t.fn);
}
OK, 到 create 了
这个 create 有点长, 看仔细了, 我们现在分析同步的部分.
create(options) {
// 初始化参数, 保存 options 到本对象 this.options, 保存 new Hook([“options”]) 传入的参数到 this._args
this.init(options);
let fn;
// 动态构建钩子, 这里是抽象层, 分同步, 异步, promise
switch (this.options.type) {
// 先看同步
case “sync”:
// 动态返回一个钩子函数
fn = new Function(
// 生成函数的参数,no before no after 返回参数字符串 xxx,xxx 在
// 注意这里 this.args 返回的是一个字符串,
// 在这个例子中是 options
this.args(),
‘”use strict”;\n’ +
this.header() +
this.content({
onError: err => `throw ${err};\n`,
onResult: result => `return ${result};\n`,
onDone: () => “”,
rethrowIfPossible: true
})
);
break;
case “async”:
fn = new Function(
this.args({
after: “_callback”
}),
‘”use strict”;\n’ +
this.header() +
// 这个 content 调用的是子类类的 content 函数,
// 参数由子类传, 实际返回的是 this.callTapsSeries() 返回的类容
this.content({
onError: err => `_callback(${err});\n`,
onResult: result => `_callback(null, ${result});\n`,
onDone: () => “_callback();\n”
})
);
break;
case “promise”:
let code = “”;
code += ‘”use strict”;\n’;
code += “return new Promise((_resolve, _reject) => {\n”;
code += “var _sync = true;\n”;
code += this.header();
code += this.content({
onError: err => {
let code = “”;
code += “if(_sync)\n”;
code += `_resolve(Promise.resolve().then(() => { throw ${err}; }));\n`;
code += “else\n”;
code += `_reject(${err});\n`;
return code;
},
onResult: result => `_resolve(${result});\n`,
onDone: () => “_resolve();\n”
});
code += “_sync = false;\n”;
code += “});\n”;
fn = new Function(this.args(), code);
break;
}
// 把刚才 init 赋的值初始化为 undefined
// this.options = undefined;
// this._args = undefined;
this.deinit();

return fn;
}
到了这个方法, 一切我们都一目了然了 (看 content 的参数), 在我们的例子中他是通过动态的生成一个 call 方法, 根据的条件有, 钩子是否有 context 属性 (这个是根据 header 的代码才能知道), 钩子的个数, 钩子的类型, 钩子的参数, 钩子的拦截器个数.
注意, 这上面有关于 fn 这个变量的函数, 返回的都是字符串, 不是函数不是方法, 是返回可以转化成代码执行的字符串, 思维要转变过来.
现在我们来看看 header()
header() {
let code = “”;
// this.needContext() 判断 taps[i] 是否 有 context 属性, 任意一个 tap 有 都会返回 true
if (this.needContext()) {
// 如果有 context 属性, 那_context 这个变量就是一个空的对象.
code += “var _context = {};\n”;
} else {
// 否则 就是 undefined
code += “var _context;\n”;
}
// 在 setup() 中 把所有 tap 对象的钩子 都给到了 instance , 这里的 this 就是 setup 中的 instance _x 就是钩子对象数组
code += “var _x = this._x;\n”;
// 如果有拦截器, 在我们的例子中, 就有一个拦截器
if (this.options.interceptors.length > 0) {
// 保存 taps 数组到_taps 变量, 保存拦截器数组 到变量_interceptors
code += “var _taps = this.taps;\n”;
code += “var _interceptors = this.interceptors;\n”;
}
// 如果没有拦截器, 这里也不会执行. 一个拦截器只会生成一次 call
// 在我们的例子中, 就有一个拦截器, 就有 call
for (let i = 0; i < this.options.interceptors.length; i++) {
const interceptor = this.options.interceptors[i];
if (interceptor.call) {
// getInterceptor 返回的 是字符串 是 `_interceptors[i]`
// 后面的 before 因为我们的拦截器没有 context 所以返回的是 undefined 所以后面没有跟一个空对象
code += `${this.getInterceptor(i)}.call(${this.args({
before: interceptor.context ? “_context” : undefined
})});\n`;
}
}
return code;
// 注意 header 返回的不是代码, 是可以转化成代码的字符串 (这个时候并没有执行).
/**
* 此时 call 函数应该为:
* “use strict”;
* function (options) {
* var _context;
* var _x = this._x;
* var _taps = this.taps;
* var _interterceptors = this.interceptors;
* // 我们只有一个拦截器所以下面的只会生成一个
* _interceptors[0].call(options);
*}
*/
}
现在到我们的 this.content() 了, 仔细一看,this.content() 方法并不在 HookCodeFactory 上, 很明显这个 content 是由子类来实现的, 往回看看这个 create 是由谁调用的? 没错, 是 SuncHookCodeFactory 的石料理, 我们来看看 SyncHook.js 上的 SyncHookCodeFactory 实现的 content
在看这个 content 实现之前, 先来回顾一下父类的 create() 给他传了什么参数.
this.content({
onError: err => `throw ${err};\n`,
onResult: result => `return ${result};\n`,
onDone: () => “”,
rethrowIfPossible: true
})
注意了, 这上面不是抛出错误, 不是返回值. 这里面的回调执行了以后返回的是一个字符串, 不要搞混了代码与可以转化成代码的字符串.
content({onError, onResult, onDone, rethrowIfPossible}) {
return this.callTapsSeries({
// 可以在这改变 onError 但是这里的 i 并没有用到, 这是什么操作 …
// 注意这里并没有传入 onResult
onError: (i, err) => onError(err),
onDone,
// 这个默认为 true
rethrowIfPossible
});
}
这个函数返回什么取决于 this.callTapSeries(), 那接下来我们来看看这个函数 ( 这层层嵌套, 其实也是有可斟酌的地方. 看源码不仅要看实现, 代码的组织也是很重要的编码能力)
刚才函数的头部已经出来了, 头部做了初始化的操作, 与生成执行拦截器代码.content 很明显, 要开始生成执行我们的 tap 对象的代码了 (如果不然, 我们的 tap 进来的函数在哪里执行呢? 滑稽:).
callTapsSeries({onError, onResult, onDone, rethrowIfPossible}) {
// 如果 taps 钩子处理完毕, 执行 onDone, 或者一个 tap 都没有 onDone() 返回的是一个字符串. 看上面的回顾就知道了.
if (this.options.taps.length === 0) return onDone();
// 如果由异步钩子, 把第一个异步钩子的下标, 如果没有这个返回的是 -1
const firstAsync = this.options.taps.findIndex(t => t.type !== “sync”);
// 定义一个函数 接受一个 number 类型的参数, i 应该是 taps 的 index
// 从这个函数的命名来看, 这个函数应该会递归的执行
// 我们先开最后的 return 语句, 发现第一个传进来的参数是 0
const next = i => {
// 如果 大于等于钩子函数数组长度, 返回并执行 onDone 回调, 就是 tap 对象都处理完了
// 跳出递归的条件
if (i >= this.options.taps.length) {
return onDone();
}
// 这个方法就是递归的关键, 看见没, 逐渐往上遍历
// 注意这里只是定义了方法, 并没有执行
const done = () => next(i + 1);
// 传入一个值 如果是 false 就执行 onDone true 返回一个 “”
// 字面意思, 是否跳过 done 应该是增加一个跳出递归的条件
const doneBreak = skipDone => {
if (skipDone) return “”;
return onDone();
};
// 这里就是处理单个 taps 对象的关键, 传入一个下标, 和一系列回调.
return this.callTap(i, {
// 调用的 onError 是 (i, err) => onError(err) , 后面这个 onError(err) 是 () => `throw ${err}`
// 目前 i done doneBreak 都没有用到
onError: error => onError(i, error, done, doneBreak),
// 这里 onResult 同步钩子的情况下在外部是没有传进来的, 刚才也提到了
// 这里 onResult 是 undefined
onResult:
onResult &&
(result => {
return onResult(i, result, done, doneBreak);
}),
// 没有 onResult 一定要有一个 onDone 所以这里就是一个默认的完成回调
// 这里的 done 执行的是 next(i+1), 也就是迭代的处理完所有的 taps
onDone:
!onResult &&
(() => {return done();}),
// rethrowIfPossible 默认是 true 也就是返回后面的
// 因为没有异步函数 firstAsync = -1.
// 所以返回的是 -1 < 0, 也就是 true, 这个可以判断当前的是否是异步的 tap 对象
// 这里挺妙的 如果是 false 那么当前的钩子类型就不是 sync, 可能是 promise 或者是 async
// 具体作用要看 callTaps() 如何使用这个.
rethrowIfPossible:
rethrowIfPossible && (firstAsync < 0 || i < firstAsync)
});
};

return next(0);
}
参数搞明白了, 现在, 我们可以进入 callTap() 了.
callTap 挺长的, 因为他也分了 3 种类型分别处理, 像 create() 一样.
/** tapIndex 下标
* onError:() => onError(i,err,done,skipdone) ,
* onReslt: undefined
* onDone: () => {return: done()} // 开启递归的钥匙
* rethrowIfPossible: false 说明当前的钩子不是 sync 的.
*/
callTap(tapIndex, { onError, onResult, onDone, rethrowIfPossible}) {
let code = “”;
// hasTapCached 是否有 tap 的缓存, 这个要看看他是怎么做的缓存了
let hasTapCached = false;
// 这里还是拦截器的用法, 如果有就执行拦截器的 tap 函数
for (let i = 0; i < this.options.interceptors.length; i++) {
const interceptor = this.options.interceptors[i];
if (interceptor.tap) {
if (!hasTapCached) {
// 这里 getTap 返回的是 _taps[0] _taps[1]… 的字符串
// 这里生成的代码就是 `var _tap0 = _taps[0]`
// 注意: _taps 变量我们在 header 那里已经生成了
code += `var _tap${tapIndex} = ${this.getTap(tapIndex)};\n`;
// 可以看到这个变量的作用就是, 如果有多个拦截器. 这里也只会执行一次.
// 注意这句获取_taps 对象的下标用的是 tapIndex, 在一次循环中, 这个 tapIndex 不会变
// 就是说如果这里执行多次, 就会生成多个重复代码, 不稳定, 也影响性能.
// 但是你又要判断拦截器有没有 tap 才可以执行, 或许有更好的写法
// 如果你能想到, 那么你就是 webpack 的贡献者了. 不过这样写, 似乎也没什么不好.
hasTapCached = true;
}
// 这里很明显跟上面的 getTap 一样 返回的都是字符串
// 我就直接把这里的 code 分析出来了, 注意 这里还是在循坏中.
// code += _interceptor[0].tap(_tap0);
// 由于我们的拦截器没有 context, 所以没传_context 进来.
// 可以看到这里是调用拦截器的 tap 方法然后传入 tap0 对象的地方
code += `${this.getInterceptor(i)}.tap(${
interceptor.context ? “_context, ” : “”
}_tap${tapIndex});\n`;
}
}
// 跑出了循坏
// 这里的 getTapFn 返回的也是字符串 `_x[0]`
// callTap 用到的这些全部在 header() 那里生成了, 忘记的回头看一下.
// 这里的 code 就是: var _fn0 = _x[0]
code += `var _fn${tapIndex} = ${this.getTapFn(tapIndex)};\n`;
const tap = this.options.taps[tapIndex];
// 开始处理 tap 对象
switch (tap.type) {
case “sync”:
// 全是同步的时候, 这里不执行, 如果有异步函数, 那么恭喜, 有可能会报错. 所以他加了个 try…catch
if (!rethrowIfPossible) {
code += `var _hasError${tapIndex} = false;\n`;
code += “try {\n”;
}
// 前面分析了 同步的时候 onResult 是 undefined
// 我们也分析一下如果走这里会怎样
// var _result0 = _fn0(option)
// 可以看到是调用 tap 进来的钩子并且接收参数
if (onResult) {
code += `var _result${tapIndex} = _fn${tapIndex}(${this.args({
before: tap.context ? “_context” : undefined
})});\n`;
} else {
// 所以会走这里
// _fn0(options) 额 … 我日 有就接受一下结果
code += `_fn${tapIndex}(${this.args({
before: tap.context ? “_context” : undefined
})});\n`;
}
// 把 catch 补上, 在这个例子中没有
if (!rethrowIfPossible) {
code += “} catch(_err) {\n”;
code += `_hasError${tapIndex} = true;\n`;
code += onError(“_err”);
code += “}\n”;
code += `if(!_hasError${tapIndex}) {\n`;
}
// 有 onResult 就把结果给传递出去. 目前没有
if (onResult) {
code += onResult(`_result${tapIndex}`);
}
// 有 onDone() 就调用他开始递归, 还记得上面的 next(i+1) 吗?
if (onDone) {
code += onDone();
}
// 这里是不上上面的 if 的大括号, 在这个例子中没有, 所以这里也不执行
if (!rethrowIfPossible) {
code += “}\n”;
}
// 同步情况下, 这里最终的代码就是
// var _tap0 = _taps[0];
// _interceptors[0].tap(_tap0);
// var _fn0 = _x[0];
// _fn0(options);
// 可以看到, 这里会递归下去
// 因为我们 tap 了 4 个钩子
// 所以这里会从复 4 次
// 最终长这样
// var _tap0 = _taps[0];
// _interceptors[0].tap(_tap0);
// var _fn0 = _x[0];
// _fn0(options);
// var _tap1 = _taps[1];
// _interceptors[1].tap(_tap1);
// var _fn1 = _x[1];
// _fn1(options);
// ……
break;
case “async”:
let cbCode = “”;
if (onResult) cbCode += `(_err${tapIndex}, _result${tapIndex}) => {\n`;
else cbCode += `_err${tapIndex} => {\n`;
cbCode += `if(_err${tapIndex}) {\n`;
cbCode += onError(`_err${tapIndex}`);
cbCode += “} else {\n”;
if (onResult) {
cbCode += onResult(`_result${tapIndex}`);
}
if (onDone) {
cbCode += onDone();
}
cbCode += “}\n”;
cbCode += “}”;
code += `_fn${tapIndex}(${this.args({
before: tap.context ? “_context” : undefined,
after: cbCode
})});\n`;
break;
case “promise”:
code += `var _hasResult${tapIndex} = false;\n`;
code += `var _promise${tapIndex} = _fn${tapIndex}(${this.args({
before: tap.context ? “_context” : undefined
})});\n`;
code += `if (!_promise${tapIndex} || !_promise${tapIndex}.then)\n`;
code += ` throw new Error(‘Tap function (tapPromise) did not return promise (returned ‘ + _promise${tapIndex} + ‘)’);\n`;
code += `_promise${tapIndex}.then(_result${tapIndex} => {\n`;
code += `_hasResult${tapIndex} = true;\n`;
if (onResult) {
code += onResult(`_result${tapIndex}`);
}
if (onDone) {
code += onDone();
}
code += `}, _err${tapIndex} => {\n`;
code += `if(_hasResult${tapIndex}) throw _err${tapIndex};\n`;
code += onError(`_err${tapIndex}`);
code += “});\n”;
break;
}
return code;
}
好了, 到了这里 我们可以把 compile 出来的 call 方法输出出来了
“use strict”;
function (options) {
var _context;
var _x = this._x;
var _taps = this.taps;
var _interterceptors = 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[1].tap(_tap1);
var _fn1 = _x[1];
_fn1(options);
var _tap2 = _taps[2];
_interceptors[2].tap(_tap2);
var _fn2 = _x[2];
_fn2(options);
var _tap3 = _taps[3];
_interceptors[3].tap(_tap3);
var _fn3 = _x[3];
_fn3(options);
}
到了这里可以知道, 我们的例子中 h1.call() 其实调用的就是这个方法. 到此我们可以说是知道了这个库的百分之 80 了.
不知道大家有没有发现, 这个生成的函数的参数列表是从哪里来的呢? 往回翻到 create() 方法里面调用的 this.args() 你就会看见, 没错就是 this._args. 这个东西在哪里初始化呢? 翻一下就知道, 这是在 Hook.js 这个类里面初始化的, 也就是说你 h1 = new xxxHook([‘options’]) 的时候传入的数组有几个值, 那么你 h1.call({name: ‘haha’}) 就能传几个值. 看教程的时候他说, 这里传入的是一个参数名字的字符串列表, 那时候我就纳闷, 什么鬼, 我传入的不是值吗, 怎么就变成了参数名称, 现在完全掌握 ….
好了, 最简单的 SyncHook 已经搞掂, 但是一看 tapable 内部核心使用的钩子却不是他, 而是 SyncBailHook, 在教程中我们已经知道,bail 是只要有一个钩子执行完了, 并且返回一个值, 那么其他的钩子就不执行. 我们来看看他是怎么实现的.
从刚才我们弄明白的 synchook, 我们知道了他的套路, 其实生成的函数的 header() 都是一样的, 这次我们直接来看看 bailhook 实现的 content() 方法
content({onError, onResult, onDone, rethrowIfPossible}) {
return this.callTapsSeries({
onError: (i, err) => onError(err),
// 看回 callTapsSeries 就知道这里传入的 next 是 done
onResult: (i, result, next) =>
`if(${result} !== undefined) {\n${onResult(
result
)};\n} else {\n${next()}}\n`,
onDone,
rethrowIfPossible
});
}
看出来了哪里不一样吗? 是的 bailhook 的 callTapsSeries 传了 onResult 属性, 我们来看看他这个 onResult 是啥黑科技
父类传的 onResult 默认是 (result) => ‘return ${result}’, 那么他这里返回的就是:

// 下面返回的是字符串,
if (xxx !== undefined) {
// 这里说明, 只要有返回值 (因为不返回默认是 undefined), 就会立即 return;
return result;
} else {
// next(); 这里返回的是一个字符串 ( 因为要生成字符串代码)
// 我在上面的注释中提到了 next 是 done 就是那个开启递归的门
// 所以如果 tap 一直没返回值, 这里就会一直 if…else.. 的嵌套下去

}
回头想想, 我们刚刚是不是分析了 capTap(), 如果我们传了 onResult 会怎样? 如果你还记得就知道, 如果有传了 onResult 这个回调, 他就会接收这个返回值. 并且会调用这个回调把 result 传出去.
而且还要注意的是,onDone 在 callTap() 的时候是处理过的, 我在贴出来一次.
onDone:!onResult && (() => {return done();})
也就是说如果我传了 onResult 那么这个 onDone 就是一个 false.
所以递归的门现在从 sync 的 onDone, 变到 syncBail 的 onResult 了
好, 现在带着这些变化去看 this.capTap(), 你就能推出现在这个 call 函数会变成这样.
“use strict”;
function (options) {
var _context;
var _x = this._x;
var _taps = this.taps;
var _interterceptors = this.interceptors;
// 我们只有一个拦截器所以下面的只会生成一个
_interceptors[0].call(options);

var _tap0 = _taps[0];
_interceptors[0].tap(_tap0);
var _fn0 = _x[0];
var _result0 = _fn0(options);

if (_result0 !== undefined) {
// 这里说明, 只要有返回值 (因为不返回默认是 undefined), 就会立即 return;
return _result0
} else {
var _tap1 = _taps[1];
_interceptors[1].tap(_tap1);
var _fn1 = _x[1];
var _result1 = _fn1(options);
if (_result1 !== undefined) {
return _result1
} else {
var _tap2 = _taps[2];
_interceptors[2].tap(_tap2);
var _fn2 = _x[2];
var _result2 = _fn2(options);
if (_result2 !== undefined) {
return _result2
} else {
var _tap3 = _taps[3];
_interceptors[3].tap(_tap3);
var _fn3 = _x[3];
_fn3(options);
}
}
}
到如今,tapable 库 已经删除了 tapable.js 文件 (可能做了一些整合, 更细分了), 只留下了钩子文件. 但不影响功能,webpack 里的 compile compilation 等一众重要插件, 都是基于 tapable 库中的这些钩子.
现在我们 require(‘tapable’) 得到的对象是这样的:
{
SyncHook: function(…){},
SyncBailHook: function(…){},

}
到此, 关于 tapable 的大部分我都解剖了一遍, 还有其他类型的 hook 如果你们愿意, 相信你们去研究一下, 也能够游刃有余.
那个, 写得有些随性, 可能会让你们觉得模糊, 但是 … 我真尽力了, 这篇改了几遍, 历时一个星期 …, 不懂就在那个评论区问我. 我看到会回复的. 共勉.
后记: 本来以为会很难, 但是越往下深入的时候发现, 大神之所以成为大神, 不是他的代码写得牛, 是他的思维牛, 没有看不懂的代码, 只有跟不上的思路, 要看懂他如何把 call 函数组织出来不难, 难的是, 他居然能想到这样来生成函数, 还可以考虑到, 拦截器钩子, 和 context 属性, 以及他的 onResult onDone 回调的判断, 架构的设计, 等等, 一步接一步. 先膜拜吧 …

退出移动版