前言
作为webpack
控制事件流的依赖库,tapable
对于我们理解webpack
、独立使用解决类似需求和方法设计参考大有裨益,本文中我将从最简单的实践开始拓展,由浅及深,由点及面,主打让大家无需下载即可看完源码。
目录
源码解析
一个基本的SyncHook
该文基于的tapable
版本为2.2.1
,我们从一个最简单的例子开始理解tapable
。
引用自不必多谈,我们以SyncHook
为例。先引用该库,接着创建一个实例,先不管里面的参数作用,我们只需要知道tap
函数用来处理响应逻辑,call
函数用来发送通知。
js
const { SyncHook } = require("tapable")
// 声明一个同步hook的实例
const testHook = new SyncHook(["args1"])
// 注册响应事件
testHook.tap("tap1", res => console.log(res))
// 发送通知
setTimeout(() => testHook.call("chipi chipi chapa chapa"))
// 控制台打印出 chipi chipi chapa chapa
接下来我们抽丝剥茧,从最核心的逻辑看起。
类图如上,SyncHook
是一个函数,但是为了便于理解,我们可以将它视作一个类(毕竟JS的类其实就是一个构造函数),它是Hook
的子类,同时关联一个工厂类的子类SyncHookCodeFactory
。
js
function SyncHook(args = [], name = undefined) {
const hook = new Hook(args, name);
hook.compile = COMPILE;
return hook;
}
SyncHook
的创建函数如上,我们可以看出来SyncHook
创建了一个Hook
实例并返回,所以本质上得到的其实是一个Hook
,这与我们的类图相符。按图索骥,我们来看看Hook
是何方神圣,当我们new一个Hook
并调用call
和tap
时,它具体做了什么呢?我们挑核心部分来看一下。
先来看一下tap
函数,本质就是对options
做了处理,新增了我们传入的属性,并将这些信息赋值给taps
数组。
js
tap(options, fn) {
if (typeof options === "string") {
options = {
name: options.trim()
};
}
options = Object.assign({ type: "sync", fn }, options);
// 将options里的函数赋值给taps,具体逻辑先不看,只需要知道函数的这个作用即可
this._insert(options);
}
再来看一下call
,Hook
在初始化时把它赋值为CALL_DELEGATE
,所以我们在调用call
的时候实际调用的是CALL_DELEGATE
,我整合了一下它的逻辑,可以看出call
在被调用时会被重新赋值成compile
的函数返回结果。
js
const CALL_DELEGATE = function(...args) {
this.call = this.compile({
taps: this.taps,
interceptors: this.interceptors,
args: this._args,
type: "sync"
});
return this.call(...args);
};
所以问题的关键就回到了compiler
上,而SyncHook
的函数代码告诉我们compiler
在该函数被调用时会被赋值COMPILE
,内容如下:
js
const factory = new SyncHookCodeFactory();
const COMPILE = function(options) {
factory.setup(this, options);
return factory.create(options);
};
SyncHookCodeFactory
的setup
和create
方法继承自HookCodeFactory
,看来答案在HookCodeFactory
这两个方法中。
HookCodeFactory
顾名思义,这个类是一个关于代码的工厂类,事实确是如此,这个类的核心就是一个函数方法new Function
,代码中充斥着很多拼接字符串的操作,因此要结合实际运行时的打点结果才好理解。
我们先来看上文中调用的setup
,其逻辑相当简单,就是将options
中的taps
的函数取出赋值给_x
属性,也就是我们上文中写的回调函数,我们只写了一个,所以taps
里只有一个元素。
js
setup(instance, options) {
instance._x = options.taps.map(t => t.fn);
}
再来看create
函数,它调用了contentWithInterceptors
函数,该函数实际上调用了子类自己的content
实现。
js
create(options) {
let fn;
switch (this.options.type) {
case "sync":
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
})
);
break;
}
return fn;
}
contentWithInterceptors(options) {
return this.content(options);
}
SyncHook
的content
实现如下,它调用了工厂类的callTapsSeries
方法,该方法的工作就是根据类型输出对应的函数方法字符串。
js
content({ onError, onDone, rethrowIfPossible }) {
return this.callTapsSeries({
onError: (i, err) => onError(err),
onDone,
rethrowIfPossible
});
}
所以最终可以得到这样一个函数,也就是我们调用call
时执行的函数,其中_fn0
函数是我们通过tap
方法注册的函数:
js
function(args1) {
"use strict";
var _context;
var _x = this._x;
var _fn0 = _x[0];
_fn0(args1);
}
至此,我们可以总结出整个tapable
的基本运行流程:
代入上述的例子,当我们执行第一句时
js
const { SyncHook } = require("tapable")
// const factory = new SyncHookCodeFactory();
执行第二句时
js
const testHook = new SyncHook(["hookName"])
// testHook._args = ["hookName"]
// testHook.compiler = (options) => {
// factory.setup(this, options);
// return factory.create(options);
// }
// testHook.tapAsync testHook.tapPromise被设置为无效的报错
执行第三句时
js
testHook.tap("tap1", res => console.log(res))
// testHook.taps = [{
// name: "tap1"
// type: "sync"
// fn: res => console.log(res)
// }]
执行第四句时
js
testHook.call("chipi chipi chapa chapa")
// factory._x = [res => console.log(res)]
// testHook.call = function(args1) {
// "use strict";
// var _context;
// var _x = [res => console.log(res)];
// var _fn0 = res => console.log(res);
// _fn0(args1);
// }
可见tapable
库对这部分功能做了有效的解耦:
HookCodeFactory
类负责处理函数生成,该工厂类不直接定义不同hook的实现,只处理options
和提供一些代码处理函数供调用;Hook
类负责处理call
和tap
等函数逻辑,具体内容后文会涉及到;- 不同的
Hook
子类主要差异在于content
函数的定义,通过影响contentWithInterceptors
逻辑间接影响生成的代码内容。
Async
Sync
Async
和Promise
实际上是Hook
类型外的另一种区分方式,决定我们tap
出来什么类型的函数结构。所以你应该不难想到,声明不同类型的Hook,声明Sync
/Async
/Promise
类型的tap
和call
函数,这三者相乘会有多少种情况...当然这些取决于实际运用时候的选择,本文会告诉你如何去进行选择。
我们先来看一下对Async
类型的处理,异步又被分为多种类型,但有了上文的基础后下面的内容很容易理解。
AsyncParallelHook
我们以AsyncParallelHook
为例,顾名思义,AsyncParallelHook
为并行的异步方法,以它为例,我们来看一下它在实现上有哪些差异。
不考虑传参的不同,我们可以直接从content
看起,可以看到这里直接调用了callTapsParallel
方法。
js
content({ onError, onDone }) {
return this.callTapsParallel({
onError: (i, err, done, doneBreak) => onError(err) + doneBreak(true),
onDone
});
}
我们将一开始的例子稍作改动:
js
testHook.tapAsync("tap1", (res, errorHandler) => {
console.log(res)
errorHandler()
})
setTimeout(() => testHook.callAsync("地瓜地瓜", () => console.log("this is callback")), 3000)
// 输出结果为:
// 地瓜地瓜
// this is callback
当我们只有一个监听事件时,该方法直接调用了callTapsSeries
,和SyncHook
的逻辑类似,最终得到以下的callAsync
函数:
js
function (args1, _callback) {
"use strict";
var _context;
var _x = this._x;
var _fn0 = _x[0];
_fn0(args1, (function(_err0) {
if(err0) {
_callback(err0);
} else {
_callback();
}
}))
}
当我们注册多个监听事件呢?比如我们添加一个监听事件,这时callTapsParallel
函数调用逻辑会发生改变,其中的条件判断和字符串拼接不做赘述,可以得到如下的callAsync
函数:
js
function (args1, _callback) {
"use strict";
var _context;
var _x = this._x;
do {
var _counter = 2;
var _done = (function() {
_callback();
})
if(_counter <= 0) break;
var _fn0 = _x[0];
_fn0(args1, (function(_err0) {
if(err0) {
if(_counter > 0) {
_callback(_err0);
_counter = 0;
}
} else {
if(--_counter === 0) _done();
}
}))
if(_counter <= 0) break;
var _fn1 = _x[1];
_fn1(args1, (function(_err1) {
if(err1) {
if(_counter > 0) {
_callback(err1);
_counter = 0;
}
} else {
if(--_counter === 0) _done();
}
}))
} while(false);
}
Promise
除了上文的同步异步外,tapable
还支持Promise
,我们着重来看一下从create
开始具体做了哪些差异化处理。
在揭晓具体实现之前,我们可以思考一下如果是自己来设计的话会如何处理。首先,和之前的格式一样,生成的依然是一个函数,函数的返回值应该是一个Promise
,所以假设testHook.x
是call
函数,那么testHook.x => new Promise
。
那么tap
如何设计呢?这里我们只考虑tapPromise
的设计,参考async
,由于Promise
的逻辑在声明时就会执行,所以我们只能设计成testHook.tapPromise("listener1", () => new Promise)
再考虑回调,async
中callback
被作为参数传入,因此这部分的实现大概率雷同,resolve
就是最好的载体。tapPromise
可以通过resolve
控制自己的then
执行,还可以通过调用callPromise
的resolve
来控制回调,最简单的处理就是把回调逻辑放到callPromise
的then
中,这样不resolve
就不会执行then
,符合我们的要求。据此,我们可以大致推断出tapPromise
和callPromise
的写法如下:
js
testHook.tapPromise("listener1", () => new Promise((resolve, reject) => {
...
resolve()
}))
testHook.callPromise().then(() => console.log("this is a call promise"))
接下来,我们尝试去反写出具体实现,先仿照上文搭个架子。
js
function (args1) {
"use strict";
var _context;
var _x = this._x;
return new Promise((function(_resolve, _reject) {
do {
var _counter = 2;
var _done = (function() {
_resolve();
})
if(_counter <= 0) break;
var _fn0 = _x[0];
var _promise0 = _fn0()
_promise0.then(() => {
if (-- _counter <= 0) _done()
})
if(_counter <= 0) break;
var _fn1 = _x[1];
var _promise1 = _fn1()
_promise1.then(() => {
if (-- _counter <= 0) _done()
})
} while(false)
})
}
对比一下实际生成的代码,我们发现多了类型校验和error
的处理等等,思路大致相同。
js
function (args1) {
"use strict";
var _context;
var _x = this._x;
return new Promise((function(_resolve, _reject) {
var _sync = true;
function _error(_err) {
if(_sync)
_resolve(Promise.resolve().then((function() { throw _err; })));
else
_reject(_err);
};
do {
var _counter = 2;
var _done = (function() {
_resolve();
})
if(_counter <= 0) break;
var _fn0 = _x[0];
var _hasResult0 = false;
var _promise0 = _fn0(test)
if (!_promise0 || !_promise0.then)
throw new Error('Tap function (tapPromise) did not return promise (returned ' + _promise0 + ')')
_promise0.then((function(_result0) {
_hasResult0 = true;
if(--_counter === 0) _done();
}), function(_err0) {
if (_hasResult0) throw _err0;
if(_counter > 0) {
_error(_err0);
_counter = 0;
}
})
if(_counter <= 0) break;
var _fn1 = _x[1];
var _hasResult1 = false;
var _promise1 = _fn1()
if (!_promise1 || !_promise1.then)
throw new Error('Tap function (tapPromise) did not return promise (returned ' + _promise1 + ')')
_promise1.then((function(_result1) {
_hasResult1 = true;
if(--_counter === 0) _done();
}), function(_err1) {
if (_hasResult1) throw _err1;
if(_counter > 0) {
_error(_err1);
_counter = 0;
}
})
} while(false);
_sync = false;
}));
}
拓展
我们接下来再简单介绍其他几种类型Hook
的实现。
AsyncParallelBailHook
对用例稍加改动。
js
testHook.tapAsync("listener1", (res1, errorHandler) => {
console.log(res1)
errorHandler(null, "1")
})
上文我们谈到AsyncParallelHook
是并行的异步hook,它的并行性表现在注册的tap
函数执行是异步的,但是callback
在所有tap
函数执行完成后才会回调;而bail
的含义为保险丝,这种BailHook
有点类似于Promise.race
,只要有一个回调函数的返回值不为null
就会执行callback
逻辑,并且其他的tap
逻辑不再执行,这是两者最大的区别。具体实现如下。
js
function (args1, _callback) {
"use strict";
var _context;
var _x = this._x;
var _results = new Array(2);
var _checkDone = function() {
for(var i = 0; i < _results.length; i++) {
var item = _results[i];
if(item === undefined) return false;
if(item.result !== undefined) {
_callback(null, item.result);
return true;
}
if (item.error) {
_callback(item.error);
return true;
}
}
return false;
}
do {
var _counter = 2;
var _done = (function() {
_callback();
})
if(_counter <= 0) break;
var _fn0 = _x[0];
_fn0(args1, (function(_err0, _result0) {
if(err0) {
if (_counter > 0) {
if(0 < _results.length && ((_results.length = 1), (_results[0] = { error: _err0 }), _checkDone())) {
_counter = 0;
} else {
if(--_counter === 0) _done();
}
}
} else {
if (_counter > 0) {
if(0 < _results.length && (_result0 !== undefined && (_results.length = 1), (_results[0] = { result: _result0 }), _checkDone()) {
_counter = 0;
} else {
if(--_counter === 0) _done();
}
}
if(--_counter === 0) _done();
}
}))
if(_counter <= 0) break;
if (1 >= _results.length) {
if(--_counter === 0) _done();
} else {
var _fn1 = _x[1];
_fn1(args1, (function(_err1, _result1) {
if(err1) {
if (_counter > 0) {
if(1 < _results.length && ((_results.length = 1), (_results[1] = { error: _err1 }), _checkDone())) {
_counter = 0;
} else {
if(--_counter === 0) _done();
}
}
} else {
if (_counter > 0) {
if(1 < _results.length && (_result1 !== undefined && (_results.length = 1), (_results[1] = { result: _result1 }), _checkDone()) {
_counter = 0;
} else {
if(--_counter === 0) _done();
}
}
if(--_counter === 0) _done();
}
}))
}
} while(false);
}
这里的_results
接收的是用户在tap
中自定义的结果,在第二个函数参数里传入,所有的tap
函数执行是串行的,一旦某个tap
中回传的结果是有效的就会立马停止执行其他函数。
AsyncSeriesHook
顾名思义,这是一个串行执行的异步hook,我们将用例代码修改成如下:
js
testHook.tapAsync("listener1", (test, cb) => {
console.log("test1", test);
cb();
})
这个hook相对简单,保证tap
函数依次执行即可,callAsync函数源码如下:
js
function(args1, _callback) {
"use strict";
var _context;
var _x = this._x;
function _next0() {
var _fn1 = _x[1];
_fn1(args1, (function(_err1) {
if(err1) {
_callback(err1);
} else {
_callback();
}
}))
}
var _fn0 = _x[0];
_fn0(args1, (function(_err0) {
if(err0) {
_callback(err0);
} else {
_next0();
}
}))
}
AsyncSeriesBailHook
根据上文我们不难理解,这个hook就是串行+block,一旦有一个tap
函数正常返回,则不再执行其他tap
函数,其实现机制和上文中的BailHook
相同。
callAsync函数源码如下:
js
function(args1, _callback) {
"use strict";
var _context;
var _x = this._x;
function _next0() {
var _fn1 = _x[1];
_fn1(args1, (function(_err1, _result1) {
if(_err1) {
_callback(_err1);
} else {
if(_result1 !== undefined) {
_callback(null, _result1);
} else {
_callback()
}
}
}
}
var _fn0 = _x[0];
_fn0(args1, (function(_err0, _result1) {
if(_err0) {
_callback(_err0);
} else {
if(_result0 !== undefined) {
_callback(null, _result1);
} else {
_next0()
}
}
}
}
AsyncSeriesLoopHook
看到loop,顾名思义,这个hook和循环相关,具体的使用场景确实也比较特别,简单说就是允许自定义逻辑里面通过控制返回值来重复执行,可以通过下面的例子来理解。
js
let count = 1;
testHook.tapAsync("listener1", (res1, errorHandler) => {
if (count < 5) {
errorHandler(null, ++ count)
} else {
errorHandler()
}
})
testHook.tapAsync("listener2", (res2, errorHandler) => {
console.log(res2)
errorHandler()
})
testHook.callAsync("chipi chipi chapa chapa", () => console.log("this is a callback"))
上述的例子中,先循环执行四次第一个tap
,在最后一个执行完成后执行第二个tap
。看下面代码也很容易理解具体实现,传入的result
不为undefined
时会循环执行所有tap
,可以运用在有重试机制的场景里。
js
function(args1, _callback) {
"use strict";
var _context;
var _x = this._x;
var _looper = (function() {
var _loopAsync = false;
var _loop;
do {
_loop = false;
function _next0() {
var _fn1 = _x[1];
_fn1(args1, (function(_err1, _result1) {
if(_err1) {
_callback(_err1);
} else {
if(_result1 !== undefined) {
_loop = true;
if (_loopAsync) _looper();
} else {
if (!_loop) {
_callback();
}
}
}
})
}
var _fn0 = _x[0];
_fn0(args1, (function(_err0, _result0) {
if(_err0) {
_callback(_err0);
} else {
if(_result0 !== undefined) {
_loop = true;
if (_loopAsync) _looper();
} else {
if (!_loop) {
_next0();
}
}
}
})
} while(_loop);
_loopAsync = true;
})
_looper();
}
AsyncSeriesWaterfallHook
看到waterfall应该很好联想该hook的作用,它会将上一个tap
的结果作为参数传入下一个tap
,代码也比较简单,直接通过修改this._args
来实现传递结果。
js
function(args1, _callback) {
"use strict";
var _context;
var _x = this._x;
function _next0() {
var _fn1 = _x[1];
_fn1(_result0, (function(_err1, _result1) {
if(_err1) {
_callback(_err1);
} else {
if(_result1 !== undefined) {
this._args[0] = _result1;
_callback();
}
}
})
}
var _fn0 = _x[0];
_fn0(args1, (function(_err0, _result0) {
if(_err0) {
_callback(_err0);
} else {
if(_result0 !== undefined) {
this._args[0] = _result0;
_next0();
}
}
})
SyncBailHook
以下的几个同步hook和异步hook的作用相同,可以直接看具体的代码。
js
function(args1) {
"use strict";
var _context;
var _x = this._x;
var _fn0 = _x[0];
var _result0 = _fn0(args1);
if(_result0 !== undefined) {
return _result0;
} else {
var _fn1 = _x[1];
var _result1 = _fn1(args1);
if(_result1 !== undefined) {
return _result1;
} else {}
}
}
SyncLoopHook
这里需要注意当tap
没有返回值时才会循环,与异步的逻辑相反。
js
function(args1) {
"use strict";
var _context;
var _x = this._x;
var _loop;
do {
_loop = false;
var _fn0 = _x[0];
var _result0 = _fn0(args1);
if(_result0 !== undefined) {
_loop = true;
} else {
if (!_loop) {
var _fn1 = _x[1];
var _result1 = _fn1(args1);
if(_result1 !== undefined) {
_loop = true;
} else {
if (!_loop) {}
}
}
}
} while(_loop);
}
SyncWaterfallHook
js
function(args1) {
"use strict";
var _context;
var _x = this._x;
var _fn0 = _x[0];
var _result0 = _fn0(args1);
if(_result0 !== undefined) {
this._args[0] = _result0;
var _fn1 = _x[1];
var _result1 = _fn1(_result0);
if(_result1 !== undefined) {
return _result1;
}
}
}
通过和上一部分sync
的比较,我们可以发现call
的类型影响的是create
逻辑,看代码其实就是影响回调的方式,同步call
无回调,异步call
有回调。
js
create(options) {
...
case "sync":
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
})
);
break;
case "async":
fn = new Function(
this.args({
after: "_callback"
}),
'"use strict";\n' +
this.header() +
this.contentWithInterceptors({
onError: err => `_callback(${err});\n`,
onResult: result => `_callback(null, ${result});\n`,
onDone: () => "_callback();\n"
})
);
break;
...
}
interceptors
顾名思义,拦截器,允许我们在指定的时机插入一些操作,它通过intercept
方法注入触发,从代码中我们可以看到有以下几个属性。
js
testHook.intercept({
// 修改输出的是注册的tap函数设置
register: opt => return opt;
// tap call和loop使用context与否的开关
context: false,
call: args => console.log(args);
// args也是注册的tap函数设置 并且允许使用context
tap: args => console.log(args);
// 剩余处理逻辑的拦截器
loop error result done
})
register
实际可能执行的时机有两处,但是在intercept
逻辑中会去判断taps
长度,在_runRegisterInterceptors
中会去判断interceptor
的长度,所以无论intercept
和tap
的顺序如何,实际只有一处会被执行。
剩余的call
等拦截器都会被写入到生成的代码中执行。
- 优先级
tapable
还额外提供了stage
和before
字段来允许用户自定义优先级,简单易懂,但是从代码来看两者无法结合使用,我们先看一个和before
相关的例子。
js
testHook.tap("listener2", (res1) => console.log("this is 2"))
testHook.tap({
name: "listener1",
before: ["listener2"]
}, () => console.log("this is 1"))
// 输出结果
// this is 1
// this is 2
再看一个stage
相关的例子。
js
testHook.tap({
name: "listener2",
stage: 4
}, () => console.log("this is 2"))
testHook.tap({
name: "listener1",
stage: 3
}, () => console.log("this is 1"))
// 输出结果
// this is 1
// this is 2
如果我们同时使用呢,这时我们需要看一下具体代码。
js
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;
假设tap1的stage
为3,tap2的stage
为4,按照stage
,tap1先于tap2执行,这时我们假设tap2的before
为包含tap1,我们演示一下上述变化。
- 直接添加tap1,taps为[tap1];
- 进入循环后i变成0,x为tap1,taps为[tap1, tap1],此时由于before.has(tap1),所以直接跳出循环,taps为[tap2, tap1]。
但是实际运用的时候伴随场景复杂很难保证不起冲突,stage
的场景其实涵盖了before
,因此无需同时使用。
整体概览
通过上面的源码分析,我们从功能角度可以梳理出以下结论:
ParalllelHook
:主要用于异步场景,允许异步hook并行,实际上是对同步hook场景下允许使用callback的补充;SeriesHook
:与上述相对的串行场景,这两者对于同步hook都是不存在的;BailHook
:提供了熔断机制,类似于race;LoopHook
:提供了循环机制,同/异步hook在细节上有所不同;WaterfallHook
:提供了瀑布流机制,上一个执行结果可以作为下一个输入参数。
更重要的是关于Sync
Async
和Promise
的使用,我们将详细谈谈这一部分在使用时如何选择。
call
的类型选择直接决定了实际生成的call
函数框架,我们可以看以下实际使用到call
type的逻辑部分。
js
switch (this.options.type) {
case "sync":
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
})
);
break;
case "async":
fn = new Function(
this.args({
after: "_callback"
}),
'"use strict";\n' +
this.header() +
this.contentWithInterceptors({
onError: err => `_callback(${err});\n`,
onResult: result => `_callback(null, ${result});\n`,
onDone: () => "_callback();\n"
})
);
break;
case "promise":
let errorHelperUsed = false;
const content = this.contentWithInterceptors({
onError: err => {
errorHelperUsed = true;
return `_error(${err});\n`;
},
onResult: result => `_resolve(${result});\n`,
onDone: () => "_resolve();\n"
});
let code = "";
code += '"use strict";\n';
code += this.header();
code += "return new Promise((function(_resolve, _reject) {\n";
if (errorHelperUsed) {
code += "var _sync = true;\n";
code += "function _error(_err) {\n";
code += "if(!_sync)\n";
code +=
"_resolve(Promise.resolve().then((function() { throw _err; })));\n";
code += "else\n";
code += "_reject(_err);\n";
code += "};\n";
}
code += content;
if (errorHelperUsed) {
code += "_sync = false;\n";
}
code += "}));\n";
fn = new Function(this.args(), code);
break;
}
看到这里应该很自然地能够想到我们之前提到的生成的代码中,最外层的函数内容实际上就是由这部分负责生成的,主要是入参和接受函数结果的方式,这部分不受我们选择的具体hook类型影响,所以即使是同步hook也可以选择callAsync
;
- 具体的内容逻辑是由
tap
类型决定的,为了避免交叉使用的情况,同步hook设置使用tapAsync
和tapPromise
无效,而异步hook不支持同步调用,这是合理的,我们以SyncHook
为例,假如我们允许使用tapAsync
,那么生成的代码会变成如下。
js
function(args1) {
"use strict";
var _context;
var _x = this._x;
var _fn0 = _x[0]
function _next0() {
var _fn1 = _x[1];
_fn1(args1, (function(_err1) {
if(_err1) {
throw _err1;
} else {}
}));
}
var _fn0 = _x[0];
_fn0(args1, (function(_err0) {
if(_err0) {
throw _err0;
} else {
_next0();
}
}));
}
乍一看这么做也没有问题,但我们和AsyncSeriesHook
比较会发现这种写法和直接用AsyncSeriesHook
没有区别,且后者还支持callback,所以对不同hook限制用法主要便于用户理解和后续迭代管理。
细节选读
- 仓库代码中存在很多子类,按照一般写法,我们会写成:
js
class A extends B {}
但是实际上,extends
是ES6支持的特性,本质上还是对构造函数进行操作,库中用了更简单的写法,直接赋值constructor
,constructor
本身返回一个实例,所以用一个返回指定实例的函数重写constructor
即可。
js
function A() {
const b = new B();
b.constructor = A;
...
return b;
}
- 当连续的同步函数数量超过22时,从后往前每22个同步函数会被编组为一个
_next
函数,mark
一下,目前还不了解用意。
js
let unrollCounter = 0;
for (let j = this.options.taps.length - 1; j >= 0; j--) {
const i = j;
const unroll = current !== onDone && 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;
}
- 防止原型污染
这是我们自己封装package
时很容易遇到的问题,特别是merge
等操作很容易被原型污染攻击,需要我们对原型链进行特别处理。
js
SyncHook.prototype = null;
结语
作为一个严谨的三方库,还有很多疑问笔者自己尚未思考清楚,有很多代码细节没有一一列举,后续会持续补充。