盐焗乳鸽还要香锅
2022/11/18阅读:25主题:默认主题
源码系列之Tapable--第一弹
源码系列之Tapable--第一弹SyncHook
Tapable
是一个灵活多变的 Hook 体系。Webpack的健壮、扩展性极强的插件架构也离不开Tapable
体系。
其实Hook的本质就是围绕着 「订阅/发布」 模式叠加各种特化逻辑,适配 Webpack 体系下复杂的事件源-处理器之间交互需求,比如:
-
有些场景需要支持将前一个处理器的结果传入下一个回调处理器; -
有些场景需要支持异步并行调用这些回调处理器。
我相信通过阅读Tapable
源码,可以让大家对Hook体系有一个更加深刻的认识,理解Webpack的插件架构,同时对订阅发布模式能有更加灵活的运用~
SyncHook
首先来看一下最简单的SyncHook
。顾名思义,是一个同步的Hooks,可以依次执行注册好的回调函数。让我们一起看一下在Tapable
中是如何实现SyncHook
的吧~
const { SyncHook } = require("../index.js");
// 1. 创建钩子实例
const sleep = new SyncHook();
// 2. 调用订阅接口注册回调
sleep.tap("test", () => {
console.log("callback A");
});
// 3. 调用发布接口触发回调
sleep.call();
1. 初始化:创建钩子实例
当我们去调用new SyncHook()
的时候,会进入到SyncHook
的构造函数:
function SyncHook(args = [], name = undefined) {
const hook = new Hook(args, name);
hook.constructor = SyncHook;
...
hook.compile = COMPILE;
return hook;
}
这里有一个关键的基类Hook,这里做了一系列变量以及方法的初始化:
class Hook {
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;
}
...
自此为止整个SyncHook
初始化完毕了。这里有一个compile
方法我们先留在后面去说。
2. 注册回调
然后接下来就是去使用tap
方法去注册事件的回调函数,这里需要注意的是,tap
是同步方法,我们继续跟踪其运行轨迹:
// Hook.js
// 此时:options是事件名称,fn是回调函数
tap(options, fn) {
this._tap("sync", options, fn);
}
...
_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);
}
我们可以看到一系列对options
的转换如下:

这里的runRegisterInterceptors
会遍历this.interceptors
,将options传入每一个拦截器,经过处理后返回一个新的options。由于这里我们并没有注册拦截器因此直接跳过。
接下来将会运行至_insert
方法中去。这里有的小伙伴对之前初始化的时候为什么要将相同的函数赋值给call
和_call
,在这里就得到答案了:其实就是拷贝了一份,用于还原。:
// Hook.js
_resetCompilation() {
this.call = this._call;
this.callAsync = this._callAsync;
this.promise = this._promise;
}
_insert(item) {
// 还原call方法
this._resetCompilation();
...
this.taps[i] = item; // 最后将之前的options插入到taps数组中去
}
}
3. 触发Hook
当sleep.call()
执行时,触发Hook.call()
即CALL_DELEGATE
:
const CALL_DELEGATE = function(...args) {
this.call = this._createCall("sync");
return this.call(...args);
};
...
// Hook
_createCall(type) {
// type = 'sync'
return this.compile({
taps: this.taps,
interceptors: this.interceptors,
args: this._args,
type: type
});
}
还记得我们在初始化的时候提到过的complie
函数嘛,complie
函数由子类构造函数(这里是SyncHook
)实现。
// HookCodeFactory.js
...
setup(instance, options) {
instance._x = options.taps.map(t => t.fn);
}
...
// SyncHook.js
const COMPILE = function(options) {
factory.setup(this, options);
return factory.create(options);
};
setup
将taps数组内重映射成了Fn[]
,并将其赋值到SyncHook._x
中
这里的
factory
是库内部封装的一个类,主要是用于生成最终的complie
函数,即最终执行的call
函数,这里先不作多讲解。
// HookCodeFactory.js
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;
}
/**
* @param {{ type: "sync" | "promise" | "async", taps: Array<Tap>, interceptors: Array<Interceptor> }} options
*/
init(options) {
this.options = options;
this._args = options.args.slice();
}
这里最终create
方法其实是用于创建compile
方法,中间的创建过程有点繁杂就不展开了,上述例子最终创建的函数如下:
(function anonymous(
) {
"use strict";
var _context;
var _x = this._x;
var _fn0 = _x[0];
_fn0();
})
可以看到其实就是去执行注册好的回调函数。至此整个SyncHook
的使用流程源码解读完毕。
-
new SyncHook(..args)
传入参数名称列表 -
通过 tap
方法注册事件并存储对应的回调函数 -
通过 call
=complie
方法依次执行注册好的回调函数-
这里要注意的就是, _insert
方法内部还支持更复杂的逻辑,包括执行顺序的控制等「具体可以看本文最下方」。
-
其实简而言之可以写成下述代码:
function syncCall() {
const callbacks = [fn1, fn2, fn3];
for (let i = 0; i < callbacks.length; i++) {
const cb = callbacks[i];
cb();
}
}
需要注意的是,complie
方法是不关注返回值的!而且要想传入参数,可以先从new SyncHook(['a'])
传入一个形参a
,它被用于HookCodeFactory.create
方法中的this.args()
,并且生成的最终complie
函数长这样:
(function anonymous(s
) {
"use strict";
var _context;
var _x = this._x;
var _fn0 = _x[0];
_fn0(s); // 这就是回调函数中的参数
})
回调函数中的参数又是会被透穿进来即如果sleep.call(1)
,则变量s
就是1
。
额外:_insert
函数的处理
先把源码贴出来:
_insert(item) {
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;
}
这里很明显的可以看出,我们需要分析的两个重要参数就是stage
和before
我们可以简单来的分析一下:
-
首先记录下 before:Set<tapItem.before>
以及tapItem.stage
的信息 -
开始遍历taps数组,从后往前。 -
首先复制一份尾元素并push进taps中,如果当前的 tapItem
无before
或者stage
信息,则直接替换掉之前复制的尾元素。 -
如果有 before
属性,则移动i指针到前一个元素,即continue
,直至匹配到before
所指的name,然后插入在它之前 -
如果有 stage
属性,它会对比当前指针指向的元素的stage
以及tapItem的stage
大小,如果大,则继续向前遍历,直至找到一个比tapItem
小的,则插入到其之后(i++
)。
-
这里官网其实没有关于
stage
属性是描述,但是我们可以从源码中去得出该属性的作用。

我是盐焗乳鸽还要香锅,喜欢我的文章欢迎关注噢,github链接https://github.com/1360151219,博客链接是strk2.cn,掘金账号https://juejin.cn/user/1812428713376845
作者介绍