前言
前面,我们通过简单认识tapable和js如何根据需求动态生成函数了解到了tapable的钩子类型以及同步钩子的具体实现。那么本文就来讲讲异步钩子。
异步钩子的简单使用
异步钩子的注册方式共三种,其中一种大家都很熟悉了,那就是tap,另外两种分别是tapPromise和tapAsync。
基本异步钩子都如此,所以本文就以AsyncParallelHook为例,从简单使用AsyncParallelHook到实现AsyncParallelHook。
tapPromise
tapPromise方式注册,和tap方式注册一样的接收两个参数,第一个参数算是个key值,第二个参数是执行函数。但tapPromise对执行函数有一定的要求,那就是要返回一个promise。
- 正确的写法:
js
const { AsyncParallelHook } = require('tapable');
const hook = new AsyncParallelHook(['name', 'age']);
hook.tapPromise('1', (name, age) => {
return new Promise(resolve => {
console.log('1', name, age);
resolve(1);
});
})
- 错误的写法:
js
const { AsyncParallelHook } = require('tapable');
const hook = new AsyncParallelHook(['name', 'age']);
hook.tapPromise('1', (name, age) => {
console.log('1', name, age);
})
这种错误的写法执行时就会报错,要求我们必须返回一个promise。
由于返回的是promise,所以他们的调用方式自然也是promise的方式。我们来看完整的例子。
js
const { AsyncParallelHook } = require('tapable');
const hook = new AsyncParallelHook(['name', 'age']);
hook.tapPromise('1', (name, age) => {
return new Promise(resolve => {
console.log('1', name, age);
resolve(1);
});
})
hook.tapPromise('2', (name, age) => {
return new Promise(resolve => {
console.log('2', name, age);
resolve(2);
});
})
hook.tapPromise('3', (name, age) => {
return new Promise(resolve => {
console.log('3', name, age);
resolve(3);
});
})
hook.promise('Rippi', 18).then(() => {
console.log('done');
});
运行结果:
tapAsync
tapAsync方式注册,同样也是接受两个参数,但有所不同的是,第二个执行函数除了默认传了我们定义的参数外,还会多接收一个callback参数,需要我们手动执行callback,这个callback就像是promise中的resolve一样,告诉tapable,这个注册的函数执行结束了。
同样的,类似上面的promise一样,tapAsync也有自己的调用方式,名为callAsync。
话不多说,直接看完整例子。
js
const { AsyncParallelHook } = require('tapable');
const hook = new AsyncParallelHook(['name', 'age']);
console.time('cost');
// 注册 异步有三种注册方式 tap tapAsync promise
hook.tapAsync('1', (name, age, callback) => {
setTimeout(() => {
console.log('1', name, age);
callback();
}, 1000);
});
hook.tapAsync('2', (name, age, callback) => {
setTimeout(() => {
console.log('2', name, age);
callback();
}, 2000);
});
hook.tapAsync('3', (name, age, callback) => {
setTimeout(() => {
console.log('3', name, age);
callback();
}, 3000);
});
// 异步钩子没有call,但有callAsync promise
hook.callAsync('Rippi', 18, () => {
console.log('done');
console.timeEnd('cost');
});
执行结果:
混合方式注册
异步钩子和同步钩子不同,它是有三种注册方式的,所以,自然的就允许我们乱来,三种注册方式混合着用。
我们先看代码。
js
const { AsyncParallelHook } = require('tapable');
const hook = new AsyncParallelHook(['name', 'age']);
console.time('cost');
hook.tap('1', (name, age) => {
console.log('1', name, age);
})
hook.tapAsync('2', (name, age, callback) => {
setTimeout(() => {
console.log('2', name, age);
callback();
}, 2000);
});
hook.tapPromise('3', (name, age) => {
return new Promise(resolve => {
setTimeout(() => {
console.log('3', name, age);
resolve(3);
}, 3000);
});
})
hook.promise('Rippi', 18).then(() => {
console.log('done');
console.timeEnd('cost');
});
看着三种方式注册好像很乱,其实不然。
- tap方式注册的,自然是直接执行,执行完就直接结束
- tapAsync执行后需要等callback调用完毕后才结束。
- tapPromise执行后需要等resolve调用完毕后才结束。
执行结果:
实现
注册方式我们都了解了,那么我们来实现下吧。
通过上一篇js如何根据需求动态生成函数文章,我们了解到其本质就是拼接字符串,而模仿实现的方法也很简单,那就是通过调试,获取到最后生成的代码,然后进行总结,最后再是根据总结出来的内容转化成代码实现。
AsyncParallelHook实现
同理,根据上面tapAsync注册方式的例子,我们先进行调试,看下最后生成的动态函数长什么样。
js
function anonymous(name, age, _callback) {
var _x = this._x;
var _counter = 3;
var _done = function () {
_callback();
};
var _fn0 = _x[0];
_fn0(name, age, function () {
if (--_counter === 0) _done();
});
var _fn1 = _x[1];
_fn1(name, age, function () {
if (--_counter === 0) _done();
});
var _fn2 = _x[2];
_fn2(name, age, function () {
if (--_counter === 0) _done();
});
}
看了上面的代码是否恍然大悟,原来callback是这样一回事,每次调用调用callback都会进行计数器的减一,当计数器减到0后,说明我们的所有注册函数都执行完了,然后执行我们callAsync里的函数。
接着,我们对比下tap生成的函数。
js
function anonymous(name, age) {
var _x = this._x;
// 注册的方法1
var _fn0 = _x[0];
_fn0(name, age);
// 注册的方法1
var _fn1 = _x[1];
_fn1(name, age);
// 注册的方法1
var _fn2 = _x[2];
_fn2(name, age);
}
对比过后,发现三个部分(参数部分、头部、内容部分)除了头部,均有所差别,那么我们继续在之前实现的代码中进行一定的判断就好了。
参数部分
参数部分我们需要加上一个判断,如果是tapAsync方式注册的,需要多一个callback的参数,这里我们把这个参数称之为after
。
js
args(config = {}) {
const { after } = config;
let allArgs = [...this.options.args];
if (after) {
allArgs = [...allArgs, after];
}
return allArgs.join(',');
}
由于args方法增加了判断,那么我们在create时也需要进行判断,针对不同情况给args传参。
js
create(options) {
this.init(options);
let fn;
switch (options.type) {
case 'sync':
fn = new Function(
this.args(),
this.header() + this.content(),
);
break;
case 'async':
fn = new Function(
// 此处传了after
this.args({ after: '_callback' }),
this.header() + this.content({ onDone: '_callback();' }),
);
break;
}
this.deInit();
return fn;
}
内容部分
内容部分,顶部需要添加一个计数器以及一个done函数。
js
callTapsParallel({ onDone }) {
const taps = this.options.taps;
let code = '';
// 添加计数器
code += `var _counter = ${taps.length};\n`;
// 定义done函数
code += `var _done = function() {
${onDone}
};\n`;
for (let j = 0; j < taps.length; j++) {
const tapContent = this.callTap(j, true);
code += tapContent;
}
return code;
}
}
callTap(tapIndex, onDone) {
let code = '';
// 取出回调函数
code += `var _fn${tapIndex} = _x[${tapIndex}];\n`;
let tapInfo = this.options.taps[tapIndex];
// 调用回调函数
switch (tapInfo.type) {
case 'sync':
code += `_fn${tapIndex}(${this.args()});\n`;
if (onDone) code += `if (--_counter === 0) _done();\n`;
break;
case 'async':
// 每一个注册函数生成的函数体
code += `_fn${tapIndex}(${this.args()}, function() {
if (--_counter === 0) _done();
});\n`;
break;
}
return code;
}
最后我们创建一个AsyncParallelHook文件,完成AsyncParallelHook类,让它调用工厂类中的callTapsParallel方法即可。
js
const Hook = require('./Hook');
const HookCodeFactory = require('./HookCodeFactory');
class AsyncParallelHookCodeFactory extends HookCodeFactory {
content(options) {
return this.callTapsParallel(options);
}
}
const factory = new AsyncParallelHookCodeFactory();
class AsyncParallelHook extends Hook {
compile(options) {
// 初始化代码工厂
factory.setup(this, options);
return factory.create(options);
}
}
module.exports = AsyncParallelHook;
到此为止,AsyncParallelHook我们就基本实现了,剩下的tapPromise注册方法,交由各位自行实现吧,只要理解了原理是拼接字符串,其实实现起来都非常简单的。
文末会附上代码仓库(包含了tapPromise的实现)。
结尾
本文通过AsyncParallelHook的两个例子认识异步钩子以及异步钩子的注册方式,最后再手动实现了AsyncParallelHook的tapAsync注册方式。基本tapable的钩子和其基本实现原理都覆盖到了,tapable剩余的钩子都大差不差,想要更深入的了解,可以进行甚多的demo书写以及源码查阅。
最后,希望大家都能有所收获,也希望看到此处的大家能给个小赞🌹