主要内容
-
tapable 使用流程
- 实例化 Hook
- 通过
tap/tapAsync/tapPromise设置hookInstance.taps - 调用
hookInstance.call(callAsync, callPromise)触发事件
-
各模块职责
- Hook -> 维护 taps
- HookCodeFactory -> 生成可执行代码 new Function
- SpecHookCodeFactory(在) -> 重写 HookCodeFactory.content 实现不同的 Hook 逻辑(执行代码)
-
模块关系
基本使用
import { SyncHook } from 'tapable';
const hook = new SyncHook(['arg1', 'arg2']);
hook.tap('process1', (...args) => {
console.log('process 1', args)
})
hook.tap({
name: 'finalProcess',
stage: 4,
}, (...args) => {
console.log('process 4', args)
})
hook.tap({
name: 'process3',
stage: 3,
}, (...args) => {
console.log('process 3', args)
})
hook.tap({
name: 'process2',
before: ['process3', 'finalProcess']
}, (...args) => {
console.log('process 2', args)
})
hook.call('1111', '2222')
// process 1 [ '1111', '2222' ]
// process 2 [ '1111', '2222' ]
// process 3 [ '1111', '2222' ]
// process 4 [ '1111', '2222' ]
实际执行代码:
源码分析
从前面的结果我们可以看出,尽管 tap 触发的顺序不同,但是在最终 call 方法执行的时候按照我们的预期顺序正确的执行了添加的 tap 处理函数。
接下来就深入源码看下是如何实现的。
核心模块
├── AsyncParallelBailHook.js
├── AsyncParallelHook.js
├── AsyncSeriesBailHook.js
├── AsyncSeriesHook.js
├── AsyncSeriesLoopHook.js
├── AsyncSeriesWaterfallHook.js
├── Hook.js
├── HookCodeFactory.js
├── HookMap.js
├── MultiHook.js
├── SyncBailHook.js
├── SyncHook.js
├── SyncLoopHook.js
├── SyncWaterfallHook.js
├── index.js
以上为 tapable 仓库目录结构。 基于我们的例子从 SyncHook 为入口进行源码分析:
SyncHook
// SyncHook.js
/*
MIT License http://www.opensource.org/licenses/mit-license.php
Author Tobias Koppers @sokra
*/
"use strict";
const Hook = require("./Hook");
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 not supported on a SyncHook");
};
const TAP_PROMISE = () => {
throw new Error("tapPromise is not supported on a SyncHook");
};
const COMPILE = function(options) {
factory.setup(this, options);
return factory.create(options);
};
// 1. SyncHook 类定义
function SyncHook(args = [], name = undefined) {
// 生成 Hook 实例
const hook = new Hook(args, name);
hook.constructor = SyncHook;
// 重写async, promise 方法
hook.tapAsync = TAP_ASYNC;
hook.tapPromise = TAP_PROMISE;
// 重写 compile 方法
hook.compile = COMPILE;
// 返回 hook
return hook;
}
SyncHook.prototype = null;
module.exports = SyncHook;
整个 SyncHook.js 代码量不大,主要依赖 Hook 类和 HookCodeFactory 类。
Hook 类
我们接着分析 Hook 类:
// Hook.js
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; // hook 名称
this.taps = []; // 维护所有的 taps 队列
this._call = CALL_DELEGATE;
this.call = CALL_DELEGATE;
this._x = undefined;
this.compile = this.compile;
this.tap = this.tap;
}
// abstract 方法,需要每个 Hook 自行实现
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
});
}
_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");
}
if (typeof options.context !== "undefined") {
deprecateContext();
}
options = Object.assign({ type, fn }, options);
this._insert(options);
}
tap(options, fn) {
this._tap("sync", options, fn);
}
_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];
// 从有到左处理
// 算法比较巧妙,从最后开始,直接将前一个元素设置为后一个元素,如果不满足条件则直接 i++,设置 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;
}
}
Object.setPrototypeOf(Hook.prototype, null);
module.exports = Hook;
为了简单已删除 async 和 promise 以及 interceptor 相关逻辑,读者感兴趣可自行查看 Hook.js 源码。
通过 Hook.js 源码可以得出以下结论:
- Hook.js 实际为一个抽象类(compile)方法需要每个子类自行实现 compile 方法 也是每个 Hooks 表现不一样的具体原因
- Hook 核心数据为 taps (_insert、tap 均为其服务)
- Hook 执行 call 的时候会先生成 call 函数(_createCall)再调用
- 初次调用 call 方法会生成实际的 call 方法,并替换
- 每当有新的 tap 发生时,call 会被重置(下次再触发时又重写生成)
- tap(options, fn) 可通过
stage和before两种方式设置 taps 队列顺序
compile & factory
基于上文第一条结论:compile 方法也是每个 Hooks 表现不一样的具体原因 我们以 SyncHook 为例看下其具体实现:
const COMPILE = function(options) {
factory.setup(this, options);
return factory.create(options);
};
-
factory.setup(this, options)
此时this指向为hook实例setup(instance, options) { instance._x = options.taps.map(t => t.fn); }只做一件事情:设置 instance._x 值为 taps 队列的具体处理函数。
-
return factory.create(options)create(options) { this.init(options); let fn; switch (this.options.type) { case "sync": fn = new Function( // 设置 before _args, after 参数 args 就是 new SyncHook 时传递的参数 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;
主要做两件事情:
- 处理 args(不同的 hook 在实际处理函数中除了
new Hooks()传递的参数还会有其他前后参数) - 通过
new Function生成实际调用函数
factory.contentWithInterceptors会调用factory.content方法实际执行的函数由
factory.content生成
总体流程
-
实例化 Hook
-
通过
tap/tapAsync/tapPromise设置hookInstance.taps -
调用
hookInstance.call(callAsync, callPromise)触发事件call -> CALL_DELEGATE -> _createCall -> compile -> factory.create -> factory.content
各模块职责
- Hook -> 维护 taps
- HookCodeFactory -> 生成可执行代码
- SpecHookCodeFactory -> 重写 HookCodeFactory.content 实现不同的 Hook 逻辑
一些实现细节
-
tap(options, fn)-
options 类型
// 非官方,个人总结 type TapOptions = | string | { name: string; before?: string | string[], stage?: number }通过
before、stage实现顺序的调整 -
options
name是必须的(顺序调整),但是name也是可以重复的,同名tap彼此不影响最终调用时 taps 为数组
-
-
每次 tap 后,下次调用 call 都会通过
_resetCompilation重置 call/callAsync/promsie 方法保证 taps 调用顺序正确,注意重置和 _createCall 时对 call 方法的处理
-
new Hook时第一个参数为string[]类型表示tap/tapAsync/tapPromsie处理方法的形参- 具体值无所谓,只要正确记录数量即可
- 处理方法基于 Hook 不同,可能在
new Hook的基础上前后添加参数。
在 webpack 中的使用
-
插件 每个插件都有一个
apply(compiler)方法,在方法内通过compiler.hooks.xxx.tap('', (compilation) => { //... })实现在特定流程上的插件处理逻辑 -
compiler 和 compilation 中都有大量的 tapable 的使用
个人业务中如果有需要动态绑定生成函数执行关系的场景,或者特定场景添加 Hook 解耦逻辑的场景均可使用。