tapable 简单介绍
本身是基于发布订阅模式思想的实现能够不依赖业务并在业务不同阶段注册事件去做某些事情webpack的plugin就是以tapable为核心实现的tapable是一个单独的包,也就意味着可以使用在任何适合的项目中
总体API概览
SyncHook 使用
const { SyncHook } = require('tapable');
// 创建实例
const hook = new SyncHook(['arg1', 'arg2']);
// 注册事件1
hook.tap('event1', (arg1, arg2) => { console.log('arg1:',arg1) });
// 注册事件2
hook.tap('event2', (arg1, arg2) => { console.log('arg2:', arg2) });
// 执行事件
hook.call('参数一', '参数二');
// 打印结果
arg1:参数一
arg2:参数二
- 创建
SyncHook的实例对象- 调用实例
tap注册事件- 调用实例
call方法, 会执行上一步的回调
源码解读
"use strict"
// 引入父类 Hook
const Hook = require('./Hook');
// 引入基础类 HookCodeFactory
const HookCodeFactory = require('./HookCodeFactory');
// 此处可先忽略,后面会讲到
class SyncHookCodeFactory extends HookCodeFactory {
content({ onError, onDone, rethrowIfPossible }) {
return this.callTapsSeries({
onError: (i, err) => onError(err),
onDone,
rethrowIfPossible
})
}
}
const factory = new SyncHookCodeFactory();
// 此处可暂时忽略
// const TAP_ASYNC = () => {
// throw new Error("tapAsync is ... SyncHook")
// }
// 此处可暂时忽略
// const TAP_PROMISE = () => {
// throw new Error("tapPromise is ... SyncHook")
// }
// 覆盖父类 Hook 中的compile 方法
const COMPILE = function(options) {
factory.setup(this, options);
return factory.create(options);
}
// 定义 SyncHook 构造函数
function SyncHook(args = [], name = undefined) {
const hook = new Hook(args, name);
hook.constructor = SyncHook;
// hook.tapAsync = TAP_ASYNV;
// hook.tapPromise = TAP_PROMISE;
hook.compile = COMPILE:
return hook;
}
// 原型链置为null, 保护原型的干净
SyncHook.prototype = null;
module.exports = SyncHook;
SyncHook 的构造函数核心三个逻辑
创建 Hook 实例覆盖 Hook 实例 compile 方法返回 Hook 实例
其他逻辑: 在继承 HookCodeFactory 的 SyncHookCodeFactory 类中添加 content 方法
注:想要理解这段代码,就要弄明白 Hook类以及HookCodeFactory类都做了些什么,为了方便理解,这里只列出有用到的方法和属性
Hook 类,也是几个主要API的父类
// 方便理解,这里会在源码基础上做些删减
const CALL_DELEGATE = function(...args) {
this.call = this._createCall("sync");
return this.call(...args);
};
class Hook {
constructor(args = [], name = undefined) {
// 接收实例化时传入的第一个参数
this._args = args;
// 接收实例化时传入的第二个参数
this.name = name;
// this.taps = [];
this._call = CALL_DELEGATE;
this.call = CALL_DELEGATE;
// this._x = underfined;
this.compile = this.compile;
this.tap = this.tap;
}
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,
})
}
// type参数可选范围 sync(同步)/async(异步)/promise(异步)
_tap(type, options, fn) {
// 参数非空判断省略
if (typeof options === 'string') {
options = {
name: options.trim()
}
}
options = Object.assign({ type, fn }, options);
// options = this._runRegisterInterceptors(options);
this._insert(options);
}
// 这里可能会有有疑惑,为什么不直接把 _tap 的函数体写在这里? 原因如下:
// 因为 Hook 类是核心 API 的父类,核心 API 中有同步也有异步
// 所以也要定义异步的注册事件的方法例如:tapAsync
tap(options, fn) {
this._tap('sync', options, fn)
}
// tapAsync(options, fn) {
// this._tap("async", options, fn);
// }
// 将_call 赋值给call
_resetCompilation() {
this.call = this._call;
}
_insert(item) {
this._resetCompilation();
// before/stage 逻辑先忽略,核心逻辑如下
let i = this.taps.length;
// 当this.taps为[]时,此时while不会走
while (i > 0) {
i--;
const x = this.taps[i]; // 获取this.taps末位下标的值
this.taps[i + 1] = x; // 相当于this.taps数组push了自身的末位下标值
i++; // 将下标改为原有this.taps的长度值(也就是末位下标+1)
break;
}
// 将传入的参数赋值给this.taps数组的某个下标
this.taps[i] = item;
}
}
结合SyncHook 的使用做逻辑梳理
创建Hook 实例时,在实例上挂载了一些属性和方法注册事件tap方法时,实际是调用this._insert方法,修改this.taps的值执行call方法时,实际是调用this.compile返回的函数,并将参数传入
注:this.compile并不是Hook类中定义的compile方法,而是SyncHook类中定义的COMPILE方法,该方法涉及到了HookCodeFactory类
HookCodeFactory 类
class HookCodeFactory {
constructor(config) {
this.config = config;
this.options = undefined;
this._args = undefined;
}
setup(instance, options) {
instance._x = options.taps.map(t => t.fn)
}
create(options) {
this.init(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;
// ... 其他先暂时省略
}
this.deinit();
return fn;
}
init(options) {
this.options = options;
this._args = options.args.slice()
}
// 将 this._args 转为字符串,并返回该字符串
args({before, after} = {}) {
let allArgs = this._args;
if (allArgs.length === 0) {
return ''
}
return allArgs.join(', ')
}
// 返回一个拼接的字符串
header() {
// 其他分支逻辑先忽略
let code = '';
code += 'var _context;\n';
code += 'var _x = this._x;\n';
return code;
}
contentWithInterceptors(options) {
// 忽略其他逻辑
// this.content 就是SyncHookCodeFactory类中定义的方法
return this.content(options)
}
// 其实是调用了HookCodeFactory中的callTapsSeries方法
// content({ onError, onDone, rethrowIfPossible }) {
// return this.callTapsSeries({
// onError: (i, err) => onError(err),
// onDone,
// rethrowIfPossible
// });
// }
// 这里为了方便理解,去掉了创建异步函数体的兼容代码等,只保留关键代码
callTapsSeries({
// 这个方法是调用create方法 new Function()时穿过来的onDone
onDone,
// ...其他先省略
}) {
let code = '';
let current = onDone;
// 循环注册事件回调函数的数组,并根据下标拼接字符串
for(let j = this.options.taps.length - 1; j >= 0; j--) {
let i = j;
const done = current;
const content = this.callTap(i, { onDone });
current = () => content;
}
code += current();
return code;
}
// 这里不关心注册事件回调函数的内部逻辑
// 只是拼接成函数执行的字符串,并将参数传递进去
// 拼接后的字符串例子:_fn1('参数一', '参数二')
callTap(tapIndex, {onDone}) {
let code = '';
code += `var _fn${tapIndex} = _x[${idx}];\n`;
const tap = this.options.taps[tapIndex];
switch(tap.type) {
case 'sync':
// 只是拼接成函数执行的字符串,并将参数传递进去
code += `_fn${tapIndex}(${this.args()});\n`;
if (onDone) {
code += onDone();
}
break;
}
return code;
}
}
结合使用的例子分析
SyncHookCodeFactory
SyncHook模块加载就会实例化SyncHookCodeFactorySyncHook的实例注册事件阶段SyncHookCodeFactory基本无关SyncHook的实例调用call方法时SyncHookCodeFactory的逻辑
- 执行setup方法,将注册事件的回调函数收集到数组中,并赋给
SyncHookCodeFactory实例_x属性- 接着执行
SyncHookCodeFactory实例create方法调用new Function创建函数- 执行
new Function, 传入参数,调用callTapsSeries最终生成所有注册事件的回调函数执行字符串
到这里SyncHook的基本使用就梳理完了,为了方便理解,放一张流程图给大家
总结
- Hook类作为父类,主要将注册事件的回调函数相关信息挂载到taps属性上
- HookCodeFactory 主要就是根据Hook的taps的属性动态生成一个函数,函数体内就是taps的所有函数执行的字符串代码
- SyncHook 覆盖Hook的compile方法,在HookCodeFactory类上添加content方法
- 每一个类都是有自己独立的功能,从代码设计上来说做到了解耦
- 使用类的extends方法实现在父类的基础上增加属性以及方法
- this指针的巧妙变化
- 动态创建函数
注:建议手写下源码,更容易理解这种思路以及代码设计技巧