tapable 是一个类似于 nodejs 的 EventEmitter 的库,主要是提供钩子函数的发布与订阅,webpack 中的许多对象都扩展自 Tapable 类。
Tapable 包暴露了许多 Hook 类,它们可以用来为插件创建钩子。
介绍
Hook 程序会编译一个最有效的方法来运行你的插件,它生成代码取决于:
- 注册插件的数量(没有、一个、多个)
- 注册的插件类型(sync、 async、 promise)
- 使用的调用方法(sync、 async、 promise)
- 参数的数量
- 是否使用拦截器 这样确保了最快的执行速度。
所有 hook 构造函数都带有一个可选参数,这是一个作为字符串的参数名列表。
const hook = new SyncHook(["arg1", "arg2", "arg3"]);
Hook 的类型
- Basic hook:简单的调用通过 tap 注册的函数。
- Bail:当任何一个通过 tap 注册的函数返回一个 non-undefined 的值时,将停止执行剩余的函数。
- Waterfall:将所有通过 tap 注册的函数链起来,并将每个函数的返回值传递给下一个函数。
- loop:当任何一个通过 tap 注册的函数返回一个 non-undefined 的值时,将重新返回到从第一个注册的函数开始执行,一直循环,直到所有的函数都返回 undefined。 此外,hook 可以是同步或异步的,为了反映这一点,有三种类型 Sync(同步),AsyncSeries(异步串行),AsyncParallel(异步并行)
拦截器
所有 hook 都提供了额外的拦截 API
hookInstance.intercept({
call: (...args) => void, // call hook
tap: (tap: Tap) => void, // tap hook
loop: (...args) => void, // loop hook
register: (tap: Tap) => Tap | undefined, // 可以修改每个 tap
})
示例
const hookObj = {
SyncHook,
SyncBailHook,
SyncWaterfallHook,
SyncLoopHook,
AsyncParallelHook,
AsyncParallelBailHook,
AsyncSeriesHook,
AsyncSeriesBailHook,
AsyncSeriesLoopHook,
AsyncSeriesWaterfallHook,
}
同步示例
设置计数器为6,tap${n}
执行后减去 n,其中 tap2 有返回值,当 num >= 0 时返回 arg1 和 arg2 的拼接字符串 ${arg1}${arg2}
,其余函数没有返回值
const testSyncHook = (hookName) => {
let num = 6;
const hookInstance = new hookObj[hookName](['arg1', 'arg2']);
hookInstance.tap('tap1', (arg1, arg2) => {
num = num - 1;
console.log('tap1', arg1, arg2, num);
});
hookInstance.tap('tap2', (arg1, arg2) => {
num = num - 2;
console.log('tap2', arg1, arg2, num);
return num <= 0 ? undefined : `${arg1}&${arg2}`;
});
hookInstance.tap('tap3', (arg1, arg2) => {
num = num - 3;
console.log('tap3', arg1, arg2, num)
});
hookInstance.call('x', 'y');
console.log('<- ----- END ----- ->\n\n')
}
SyncHook
testSyncHook('SyncHook');
基础的 hook。输出结果按照注册的顺序输出,且 num 最后为 0
SyncBailHook
testSyncHook('SyncBailHook');
可中断的 hook。tap2 执行后返回值为 'x&y' 不是 undefined 所以停止执行后续的函数
SyncWaterfallHook
testSyncHook('SyncWaterfallHook');
同步瀑布 hook,函数执行后在有返回值的情况下,该返回值会作为下一个注册函数的参数传入。可以注册多个函数来对结果进行加工;tap3 的第一个参数变成 tap2 的返回值 'x&y'
SyncLoopHook
testSyncHook('SyncLoopHook');
可循环的 hook,如果存在返回值不为 undefined(不能是 null 或者 '')的情况会一直循环,直到所有返回值为 undefined 才结束。tap2 第一次执行后返回 'x&y',重新回到第一个函数开始执行,第二次 tap2 执行后返回 undefined 后,继续执行后续函数
异步示例
要正常使用异步 Hook 类,需要将 tap 和 call 都对应使用异步。
AsyncParallelHook
异步并行 hook,当所有的异步任务都执行完之后最后执行 callAsync 或者 promise 中的函数
console.time('time')
const asyncParallelHook = new AsyncParallelHook(['arg1', 'arg2']);
asyncParallelHook.tapAsync('tap1', (arg1, arg2, next) => {
setTimeout(() => {
console.log('tap1', arg1, arg2);
next();
}, 3000)
})
asyncParallelHook.tapAsync('tap2', (arg1, arg2, next) => {
setTimeout(() => {
console.log('tap2', arg1, arg2);
next();
}, 2000)
})
asyncParallelHook.tapAsync('tap3', (arg1, arg2, next) => {
setTimeout(() => {
console.log('tap3', arg1, arg2);
next();
}, 1000)
})
asyncParallelHook.callAsync('x', 'y', () => {
console.log('done!')
console.timeEnd('time')
})
同时开始执行异步任务,各自完成各自的任务,等到所有任务都完成之后执行最后回调任务。
AsyncSeriesHook
异步串行 hook,按注册顺序执行所有的异步任务
console.time('time')
const asyncSeriesHook = new AsyncSeriesHook(['arg1', 'arg2']);
asyncSeriesHook.tapAsync('tap1', (arg1, arg2, next) => {
setTimeout(() => {
console.log('tap1', arg1, arg2);
next();
}, 3000)
})
asyncSeriesHook.tapAsync('tap2', (arg1, arg2, next) => {
setTimeout(() => {
console.log('tap2', arg1, arg2);
next();
}, 2000)
})
asyncSeriesHook.tapAsync('tap3', (arg1, arg2, next) => {
setTimeout(() => {
console.log('tap3', arg1, arg2);
next();
}, 1000)
})
asyncSeriesHook.callAsync('x', 'y', () => {
console.log('done!')
console.timeEnd('time')
})
执行结果会按照注册的顺序逐步开始执行,上一个任务完成后才开始下一个任务,所有任务完成之后执行最后回调任务。相当于是将异步任务进行了同步执行
AsyncParallelBailHook
异步并行可中断 hook,当存在返回值时就中断任务,执行回调
console.time('time')
const asyncParallelBailHook = new AsyncParallelBailHook(['arg1', 'arg2']);
asyncParallelBailHook.tapAsync('tap1', (arg1, arg2, callback) => {
setTimeout(() => {
console.log('tap1', arg1, arg2);
callback(undefined, undefined);
}, 3000)
})
asyncParallelBailHook.tapAsync('tap2', (arg1, arg2, callback) => {
setTimeout(() => {
console.log('tap2', arg1, arg2);
callback(undefined, 'return tap2');
}, 2000)
})
asyncParallelBailHook.tapAsync('tap3', (arg1, arg2, callback) => {
setTimeout(() => {
console.log('tap3', arg1, arg2);
callback(undefined, 'return tap3');
}, 1000)
})
asyncParallelBailHook.callAsync('x', 'y', (err, result) => {
console.log('done!')
console.log(err, result)
console.timeEnd('time')
})
因为是异步并发,中断并不能取消,而是仅使用中断那个任务的结果,而且是按照注册的顺序取中断结果,示例中的 tap3 最先执行完而且也有返回值,但是 tap2 优先于 tap3 注册,所以最后回调结果中用的是 tap2 的返回值;需要等到所有任务完成之后才能按照注册顺序看是否有返回,所以耗时是最大任务的耗时:3 秒
AsyncSeriesBailHook
异步串行可中断 hook,当存在返回值时就中断任务,执行回调
console.time('time')
const asyncSeriesBailHook = new AsyncSeriesBailHook(['arg1', 'arg2']);
asyncSeriesBailHook.tapAsync('tap1', (arg1, arg2, callback) => {
setTimeout(() => {
console.log('tap1', arg1, arg2);
callback(undefined, undefined);
}, 3000)
})
asyncSeriesBailHook.tapAsync('tap2', (arg1, arg2, callback) => {
setTimeout(() => {
console.log('tap2', arg1, arg2);
callback(undefined, 'return tap2');
}, 2000)
})
asyncSeriesBailHook.tapAsync('tap3', (arg1, arg2, callback) => {
setTimeout(() => {
console.log('tap3', arg1, arg2);
callback(undefined, 'return tap3');
}, 1000)
})
asyncSeriesBailHook.callAsync('x', 'y', (err, result) => {
console.log('done!')
console.log(err, result)
console.timeEnd('time')
})
由于是串行,按照注册顺序排队执行,遇到中断时就可以取消后续操作
AsyncSeriesLoopHook
异步串行可循环 hook,和 SyncLoopHook 功能一样,只是可以执行异步任务
let num = 6;
console.time('time')
const asyncSeriesLoopHook = new AsyncSeriesLoopHook(['arg1', 'arg2']);
asyncSeriesLoopHook.tapAsync('tap1', (arg1, arg2, callback) => {
setTimeout(() => {
console.log('tap1', arg1, arg2);
num -= 1;
callback(undefined, undefined);
}, 3000)
})
asyncSeriesLoopHook.tapAsync('tap2', (arg1, arg2, callback) => {
setTimeout(() => {
console.log('tap2', arg1, arg2);
num -= 1;
callback(undefined, undefined);
}, 2000)
})
asyncSeriesLoopHook.tapAsync('tap3', (arg1, arg2, callback) => {
setTimeout(() => {
console.log('tap3', arg1, arg2);
num -= 1;
callback(undefined, num <= 0 ? undefined : 'return tap3');
}, 1000)
})
asyncSeriesLoopHook.callAsync('x', 'y', (err, result) => {
console.log('done!')
console.log(err, result)
console.timeEnd('time')
})
需要等待每个异步任务完成后再开始下一个,tap3 第一次返回不为 undefined,第二次为 undefined,总耗时:12秒
AsyncSeriesWaterfallHook
异步串行瀑布 hook,和 SyncWaterfallHook 功能一样,只是可以执行异步任务
console.time('time')
const asyncSeriesWaterfallHook = new AsyncSeriesWaterfallHook(['arg1', 'arg2']);
asyncSeriesWaterfallHook.tapAsync('tap1', (arg1, arg2, callback) => {
setTimeout(() => {
console.log('tap1', arg1, arg2);
callback(undefined, undefined);
}, 3000)
})
asyncSeriesWaterfallHook.tapAsync('tap2', (arg1, arg2, callback) => {
setTimeout(() => {
console.log('tap2', arg1, arg2);
callback(undefined, `${arg1}&${arg2}`);
}, 2000)
})
asyncSeriesWaterfallHook.tapAsync('tap3', (arg1, arg2, callback) => {
setTimeout(() => {
console.log('tap3', arg1, arg2);
callback(undefined, 'return tap3');
}, 1000)
})
asyncSeriesWaterfallHook.callAsync('x', 'y', (err, result) => {
console.log('done!')
console.log(err, result)
console.timeEnd('time')
})
源码分析
源码中所有的 hook 都是基于 Hook 类 和 HookCodeFactory 类来实现的,主要分析这两个类即可
Hook.js
constructor
const CALL_DELEGATE = function (...args) {
this.call = this._createCall("sync");
return this.call(...args);
};
const CALL_ASYNC_DELEGATE = function (...args) {
this.callAsync = this._createCall("async");
return this.callAsync(...args);
};
const PROMISE_DELEGATE = function (...args) {
this.promise = this._createCall("promise");
return this.promise(...args);
};
constructor(args = [], name = undefined) {
this._args = args;
this.name = name;
this.taps = [];
this.interceptors = [];
this._call = CALL_DELEGATE;
this.call = CALL_DELEGATE;
this._callAsync = CALL_ASYNC_DELEGATE;
this.callAsync = CALL_ASYNC_DELEGATE;
this._promise = PROMISE_DELEGATE;
this.promise = PROMISE_DELEGATE;
this._x = undefined;
this.compile = this.compile;
this.tap = this.tap;
this.tapAsync = this.tapAsync;
this.tapPromise = this.tapPromise;
}
compile(options) {
throw new Error("Abstract: should be overridden");
}
_createCall(type) {
return this.compile({
taps: this.taps,
interceptors: this.interceptors,
args: this._args,
type: type
});
}
- 构造函数中记录了参数名称数组(args),注册的 tap 数组(taps),拦截器数组(interceptors)
- call、callAsync、promise 分别指向了函数 *_DELEGATE,这几个函数都调用了需要重新实现的 compile 函数。在其他 ****Hook 类中都重写了 compile 函数,重写的函数中调用了 HookCodeFactory 类中的方法来动态生成 call、callAsync、promise 的函数体
tap
tap(options, fn) {
this._tap("sync", options, fn);
}
tapAsync(options, fn) {
this._tap("async", options, fn);
}
tapPromise(options, fn) {
this._tap("promise", options, fn);
}
_tap(type, options, fn) {
if (typeof options === "string") {
options = {
name: options.trim()
};
} else if (typeof options !== "object" || options === null) {
throw new Error("Invalid tap options");
}
if (typeof options.name !== "string" || options.name === "") {
throw new Error("Missing name for tap");
}
options = Object.assign({ type, fn }, options);
options = this._runRegisterInterceptors(options);
this._insert(options);
}
_runRegisterInterceptors(options) {
for (const interceptor of this.interceptors) {
if (interceptor.register) {
const newOptions = interceptor.register(options);
if (newOptions !== undefined) {
options = newOptions;
}
}
}
return options;
}
- tap、tapAsync、tapPromise 最终都调用内部的 _tap 函数,通过传递不同的 type 区分,这里生成的 options 为 { type, name, fn },这个 options 就是属性 taps 中的一个值(在 this._insert(options) 中可以看到)
- 在执行 this._insert(options) 之前调用了 this._runRegisterInterceptors(options) ,说明每次在注册函数时遍历了所有的拦截器,并调用其中的 register 方法来重新处理当前注册的这个 options(即:tap[ i ] )
_insert
_resetCompilation() {
this.call = this._call;
this.callAsync = this._callAsync;
this.promise = this._promise;
}
_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;
}
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]);
}
}
}
- _insert、intercept 都对 this.call、this.callAsync、this.promise 重新赋值(this._resetCompilation()),使这几个函数等于初始的 *_DELEGATE ,这样做是因为在每次 interceptors、taps 的值有插入时都需要重新生成新的触发函数(call/callAsync/promise),才能在实体调用这些函数时实现例子中的链式触发、循环触发等需求,所以只要不新增 interceptors 或 taps 中的值,call/callAsync/promise 的函数体就不会发生变化
- _insert 函数主要是将传入的 options 添加到 taps 中,通过 options 中的 before/stage 字段来调整在 taps 中存放的位置
HookCodeFactory.js
整个 HookCodeFactory 类里面使用 Function
构造函数来生成 Function 对象(即函数),语法:
new Function ([arg1[, arg2[, ...argN]],] functionBody)
arg1, arg2, ... argN:
被函数使用的参数的名称必须是合法命名的。参数名称是一个有效的JavaScript标识符的字符串,或者一个用逗号分隔的有效字符串的列表;例如 “×”,“theValue”,或 “a, b”。functionBody:
一个含有包括函数定义的 JavaScript 语句的字符串。 由Function
构造器创建的函数不会创建当前环境的闭包,它们总是被创建于全局环境,因此在运行时它们只能访问全局变量和自己的局部变量,不能访问它们被Function
构造器创建时所在的作用域的变量。
const fn1 = () => {
const v1 = 'fn1';
return function () {
return v1;
}
}
const fn2 = () => {
const v2 = 'fn2';
return new Function('return v2;');
}
console.log(fn1()()); // fn1
console.log(fn2()()); // v2 is not defined
const adder = new Function("a", "b", "c", "return a + b + c");
adder(2, 6, 8); // 16
在 HookCodeFactory 中核心的几个函数如下
class HookCodeFactory {
contentWithInterceptors(options) { }
callTap(tapIndex, { onError, onResult, onDone, rethrowIfPossible }) { }
callTapsSeries({ onError, onResult, resultReturns, onDone, doneReturns, rethrowIfPossible }) { }
callTapsLooping({ onError, onDone, rethrowIfPossible }) { }
callTapsParallel({ onError, onResult, onDone, rethrowIfPossible, onTap = (i, run) => run() }) { }
}
- callTap:基础执行 tap 任务
- callTapsSeries:串行执行
- callTapsParallel:并行执行
- callTapsLooping:循环执行
COMPILE
SyncHook 类中重新了 compile 方法
const COMPILE = function (options) {
factory.setup(this, options);
return factory.create(options);
};
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;
}
- 其他 Hook 子类中具体的 compile 实现基本都使用如上所示的 COMPILE 函数,他们有不同的 factory 实例,这些实例的构造函数都继承至 HookCodeFactory 类,但分别实现了自己的 content 函数,这些 content 函数调用父类的
callTapsSeries、callTapsParallel、callTapsLooping
函数来生成不同的函数体 - factory.setup 将 Hook 基类构造函数中的 this._x = undefined; 重新赋值为所有通过 tap 注册的 fn 数组
- factory.create 生成最终的
call、callAsync、promise
函数
contentWithInterceptors
...
args({ before, after } = {}) {
let allArgs = this._args;
if (before) allArgs = [before].concat(allArgs);
if (after) allArgs = allArgs.concat(after);
if (allArgs.length === 0) {
return "";
} else {
return allArgs.join(", ");
}
}
header() {
let code = "";
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";
}
return code;
}
contentWithInterceptors(options) {
if (this.options.interceptors.length > 0) {
const onError = options.onError;
const onResult = options.onResult;
const onDone = options.onDone;
let code = "";
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()});\n`;
}
}
code += this.content(
Object.assign(options, {
onError:
onError &&
(err => {
let code = "";
for (let i = 0; i < this.options.interceptors.length; i++) {
const interceptor = this.options.interceptors[i];
if (interceptor.error) {
code += `${this.getInterceptor(i)}.error(${err});\n`;
}
}
code += onError(err);
return code;
}),
onResult:
onResult &&
(result => {
let code = "";
for (let i = 0; i < this.options.interceptors.length; i++) {
const interceptor = this.options.interceptors[i];
if (interceptor.result) {
code += `${this.getInterceptor(i)}.result(${result});\n`;
}
}
code += onResult(result);
return code;
}),
onDone:
onDone &&
(() => {
let code = "";
for (let i = 0; i < this.options.interceptors.length; i++) {
const interceptor = this.options.interceptors[i];
if (interceptor.done) {
code += `${this.getInterceptor(i)}.done();\n`;
}
}
code += onDone();
return code;
})
})
);
return code;
} else {
return this.content(options);
}
}
getInterceptor(idx) {
return `_interceptors[${idx}]`;
}
fn = new Function(
this.args(),
'"use strict";\n' +
this.header() +
this.contentWithInterceptors({
onError: err => `throw ${err};\n`,
onResult: result => `return ${result};\n`,
resultReturns: true,
onDone: () => "",
rethrowIfPossible: true
})
);
...
- args 方法列出所有参数,通过逗号连接成字符串返回,就像 "a, b, c"
- header 方法定义了两个变量 _context、_x,并将 this._x 赋值给了 _x,在有拦截器的情况下对 this.taps 和 this.interceptors 两个变量进行了重新赋值
- contentWithInterceptors 方法的参数是一个 options 对象,对象中有几个方法(onError、onResult、onDone)返回的都是字符串(注意这里的字符串,因为整个构造函数体是以字符串的形式存在的,所有的其他可执行代码都是为了生成最终的函数体片段),例如下面这段代码可以打印出数字 101~110
注意:const baseV = 50; let code = ''; code += 'for(let i=1; i<=10; i++) { console.log('; code += baseV * 2; code += ' + i);}' const fn = new Function(code); fn();
code += baseV * 2;
不能写成code += 'baseV * 2';
,因为作用域的问题拿不到baseV
,实际上 code 的代码为'for(let i=1; i<=10; i++) { console.log(100 + i);}'
而不是'for(let i=1; i<=10; i++) { console.log(baseV * 2 + i);}'
- 当没有拦截器时直接调用子类的 content 方法(将 options 作为参数传入);存在拦截器时用遍历的方式生成所有的拦截器的 call 方法的调用,然后重新遍历生成新的 options 后再调用 content 方法,假设有3个拦截器,初始化的参数为 'arg1, arg2, arg3' 这部分代码生成的代码段相当于如下代码(每次 error、result、done 调用前需要判断是否存在,下面代码默认都存在):
_interceptors[0].call(arg1, arg2, arg3); _interceptors[1].call(arg1, arg2, arg3); _interceptors[2].call(arg1, arg2, arg3); nextOptions = { onError: err => { const code = ` _interceptors[0].error(${err}); _interceptors[1].error(${err}); _interceptors[2].error(${err}); throw ${err}; `; return code; }, onResult: result => { const code = ` _interceptors[0].result(${result}); _interceptors[1].result(${result}); _interceptors[2].result(${result}); return ${result}; `; return code; }, onDone: () => { const code = ` _interceptors[0].done(); _interceptors[1].done(); _interceptors[2].done(); `; return code; } } this.content(Object.assign(options, nextOptions));
在 HookCodeFactory 的 子类 SyncHookCodeFactory 中的 content 函数如下:
content({ onError, onDone, rethrowIfPossible }) {
return this.callTapsSeries({
onError: (i, err) => onError(err),
onDone,
rethrowIfPossible
});
}
对于 SyncHook,只传递了3个参数给 callTapsSeries 函数,而原本 contentWithInterceptors 中的 options 有5个参数,其余的参数在其他 hook 中有用到(如:SyncBailHook 使用了全部参数);同样 onError: (i, err) => onError(err)
中没有用到 i 参数,是因为在 callTapsSeries 函数中调用的时候传递了4个参数(onError(i, error, done, doneBreak)
),而这里要接 err参数,使用第一个 i 参数占位。
callTapsSeries
callTapsSeries 是目前遇到的第一个比较核心的函数,可以看到里面调用了 callTap(在 callTapsParallel 里面也调用了这个函数)
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;
let code = "";
let current = onDone;
let unrollCounter = 0;
for (let j = this.options.taps.length - 1; j >= 0; j--) {
const i = j;
const unroll =
current !== onDone &&
(this.options.taps[i].type !== "sync" || unrollCounter++ > 20);
if (unroll) {
unrollCounter = 0;
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;
}
将 taps 数组做了一个倒序循环,可以看到第一次肯定不会执行if (unroll) {...}
里面的内容(因为第一次 current = onDone),而且保证 type !== "sync"
(异步注册 tapAsync、tapPromise) 时才会执行,只要执行 if 后就会改变 current,改变之后下一次循环进入的时候 current !== onDone
就为真了,所以可以看出如果全部是异步注册任务,除了第一次 for 循环不执行 if 语句块内容以外,其他后续循环均会执行
在全部是异步 tap 注册的情况下简化代码如下:
callTapsSeries(...) {
let current = onDone; // AsyncSeriesHook 中 onDone: () => "_callback();\n"
for (let i = 末尾; i >= 0; i--) {
if (不是第一次进入) {
"生成一个 _next_i 的函数,函数体为执行 current 函数"
current = _next_i();
}
const done = current;
content = this.callTap(i, { onDone: !onResult && done, ...});
current = () => content;
}
code += current();
}
可以看到通过 for 循环不停的对 current 重新赋值和将 current 作为参数传入 callTap 函数,最终可以实现链式调用。
当全部是同步 tap 注册的情况下始终不会执行 if 语句块
callTapsSeries(...) {
let current = onDone; // SyncHook 中 onDone: () => ""
for (let i = 末尾; i >= 0; i--) {
const done = current;
content = this.callTap(i, { onDone: !onResult && done, ...});
current = () => content;
}
code += current();
}
callTap
在 callTap 中首先使用所有已经注册拦截器中的 tap hook 对当前 tap 进行处理
let hasTapCached = false;
for (let i = 0; i < this.options.interceptors.length; i++) {
const interceptor = this.options.interceptors[i];
if (interceptor.tap) {
if (!hasTapCached) {
code += `var _tap${tapIndex} = ${this.getTap(tapIndex)};\n`;
hasTapCached = true;
}
code += `${this.getInterceptor(i)}.tap(${interceptor.context ? "_context, " : ""
}_tap${tapIndex});\n`;
}
}
这段代码中的 code += 'var _tap${tapIndex} = ${this.getTap(tapIndex)};\n;'
这句代码可以提到 for 循环外面后就可以不用使用 hasTapCached
字段了,但是这样的话对于没有 interceptors 的情况下,就会产生提出去的那句无用代码,考虑得十分周到。但是为什么非要声明一个 _tap${tapIndex}
而不是直接传入,而且也不需要 hasTapCached
如:
for (let i = 0; i < this.options.interceptors.length; i++) {
const interceptor = this.options.interceptors[i];
if (interceptor.tap) {
code += `${this.getInterceptor(i)}.tap(${interceptor.context ? "_context, " : ""}
${this.getTap(tapIndex)});\n`;
}
}
这里暂时没看懂声明这个变量的特殊用途,猜测是因为异步导致总是拿到最后一个tapIndex
callTap 中对不同 type 分别做了处理,对于 sync 逻辑比较简单,根据不同的 rethrowIfPossible、onResult、onDone
分支处理,里面做了 tap 所注册的 fn 函数的最终调用,对于 async:
code += `var _fn${tapIndex} = ${this.getTapFn(tapIndex)};\n`;
const tap = this.options.taps[tapIndex];
case "async":
let cbCode = "";
if (onResult)
cbCode += `(function(_err${tapIndex}, _result${tapIndex}) {\n`;
else cbCode += `(function(_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;
里面的 cbCode 生成了一个匿名函数通过 this.args 挂到了 fn 的最后一个参数上,这样我们使用时在构造函数中只传递了 N 个参数,但是可以使用 N + 1 个参数,且最后一个参数是个回调函数;cbCode 这个函数里面调用了传递进来的 onDone,在异步 tap 注册中 callTapsSeries 里面生成的 next_i,就是这么巧妙的链在了一起!!!对于有三个 tapAsync 注册的 AsyncSeriesHook 代码生成如下:
function anonymous(arg1, arg2, _callback) {
"use strict";
var _context;
var _x = this._x;
function _next1() {
var _fn2 = _x[2];
_fn2(arg1, arg2, function (_err2) {
if (_err2) {
_callback(_err2);
} else {
_callback();
}
});
}
function _next0() {
var _fn1 = _x[1];
_fn1(arg1, arg2, (function (_err1) {
if (_err1) {
_callback(_err1);
} else {
_next1();
}
}));
}
var _fn0 = _x[0];
_fn0(arg1, arg2, (function (_err0) {
if (_err0) {
_callback(_err0);
} else {
_next0();
}
}));
}
结合上面的 callTapsSeries 和 callTap 函数,第一次 for 循环时 i = 2,不会进入 if 语句,通过 callTap 生成 _next1 的函数体;进入第二次 for 循环时生成完整的 _next1 函数,并将 _next1 传入第二次 callTap,生成 _next0 的函数体(里面调用了 _next1);然后一直循环直到所有结束。这种来回传递参数,最终形成链式调用简直歪瑞古德!promise部分就多个then,catch,逻辑大致差不多。
callTapsParallel
callTapsParallel({ onError, onResult, onDone, rethrowIfPossible, onTap = (i, run) => run() }) {
let code = "";
code += "do {\n";
code += `var _counter = ${this.options.taps.length};\n`;
if (onDone) {
code += "var _done = (function() {\n";
code += onDone();
code += "});\n";
}
for (let i = 0; i < this.options.taps.length; i++) {
const done = () => {
if (onDone) return "if(--_counter === 0) _done();\n";
else return "--_counter;";
};
const doneBreak = skipDone => {
if (skipDone || !onDone) return "_counter = 0;\n";
else return "_counter = 0;\n_done();\n";
};
code += "if(_counter <= 0) break;\n";
code += onTap(
i,
() => this.callTap(i, {
onError: error => {
let code = "";
code += "if(_counter > 0) {\n";
code += onError(i, error, done, doneBreak);
code += "}\n";
return code;
},
onResult: onResult && (result => {
let code = "";
code += "if(_counter > 0) {\n";
code += onResult(i, result, done, doneBreak);
code += "}\n";
return code;
}),
onDone: !onResult && (() => { return done(); }),
rethrowIfPossible
}),
done,
doneBreak
);
}
code += "} while(false);\n";
return code;
}
这段代码的核心在于使用 do { break; }while(false)
结构来做全部完成的判断,通过 _counter 作为控制器,每次完成一个异步任务执行之后都减 1 ,然后判断是否完成全部任务;每次循环都加了语句code += "if(_counter <= 0) break;\n";
,用于 doneBreak
函数执行后的中断操作。
callTapsLooping
callTapsLooping({ onError, onDone, rethrowIfPossible }) {
if (this.options.taps.length === 0) return onDone();
const syncOnly = this.options.taps.every(t => t.type === "sync");
let code = "";
if (!syncOnly) {
code += "var _looper = (function() {\n";
code += "var _loopAsync = false;\n";
}
code += "var _loop;\n";
code += "do {\n";
code += "_loop = false;\n";
for (let i = 0; i < this.options.interceptors.length; i++) {
const interceptor = this.options.interceptors[i];
if (interceptor.loop) {
code += `${this.getInterceptor(i)}.loop(${this.args({
before: interceptor.context ? "_context" : undefined
})});\n`;
}
}
code += this.callTapsSeries({
onError,
onResult: (i, result, next, doneBreak) => {
let code = "";
code += `if(${result} !== undefined) {\n`;
code += "_loop = true;\n";
if (!syncOnly) code += "if(_loopAsync) _looper();\n";
code += doneBreak(true);
code += `} else {\n`;
code += next();
code += `}\n`;
return code;
},
onDone:
onDone && (() => {
let code = "";
code += "if(!_loop) {\n";
code += onDone();
code += "}\n";
return code;
}),
rethrowIfPossible: rethrowIfPossible && syncOnly
});
code += "} while(_loop);\n";
if (!syncOnly) {
code += "_loopAsync = true;\n";
code += "});\n";
code += "_looper();\n";
}
return code;
}
和 callTapsParallel 结构差不多,改变了控制变量、增加了递归处理,可以使其一直循环下去。
代码里面有很多错误处理和控制判断没有提到,需要到具体代码中去查看,通过改变onError、onResult、resultReturns、onDone、doneReturns、rethrowIfPossible
的参数可以实现所有的 hook,如果不用这种动态生成函数的方式,每个 tap、call 都由子类去实现的话,会写出很多个函数,而且很多函数还类似。