推开Webpack事件流大门——Tapable
前言
Tapable是一个由Webpack团队维护的事件库。它基于发布订阅模式实现事件处理,可以在Webpack的打包流程中插入自定义逻辑,解耦不同插件之间的处理流程。Webpack中Compiler和Compilation是Webpack内置对象,它们都继承于Tapable,通过使用Tapable来触发事件,从而将不同插件串联起来。我们可以使用Webpack提供的事件钩子来在插件中添加对应的事件处理逻辑。废话少说,入正题!
Hook Types
下面介绍下Hooks的几种类型的概念。
-
Waterfall(瀑布类型)将上一个函数的返回值传递给下一个函数的第一个参数,如果没有返回值,则使用call函数的参数。
-
Bail(保险类型)如果有返回值时,则退出任务队列。
-
Loop(循环类型)如果有函数的返回值不是undefined,则从头开始执行。
-
Sync(同步)函数顺序执行。
-
AsyncSeries(异步串行)异步函数按顺序执行。
-
AsyncParallel(异步并行)异步函数并行。
Hooks
SyncHook
同步顺序执行
import { SyncHook } from "./lib";
const syncHook = new SyncHook(["name", "age"]);
syncHook.tap("test1", (name, age) => {
console.log("test1", name, age);
});
syncHook.tap("test2", (name, age) => {
console.log("test2", name, age);
});
syncHook.tap("test3", (name, age) => {
console.log("test3", name, age);
});
syncHook.call("szy", "18");
test1 szy 18
test2 szy 18
test3 szy 18
SyncBailHook
同步顺序执行,返回结果后不往下执行
import {SyncBailHook} from './lib';
const syncBailHook = new SyncBailHook(["name", "age"]);
syncBailHook.tap("test1", (name, age) => {
console.log("test1", name, age);
});
syncBailHook.tap("test2", (name, age) => {
console.log("test2", name, age);
return 'result'
});
syncBailHook.tap("test3", (name, age) => {
console.log("test3", name, age);
});
syncBailHook.call("szy", "18");
test1 szy 18
test2 szy 18
SyncWaterfallHook
同步顺序执行,返回结果是下一个函数的第一个参数,无返回则是call的参数
import { SyncWaterfallHook } from "./lib";
const syncWaterfallHook = new SyncWaterfallHook(["name", "age"]);
syncWaterfallHook.tap("test1", (name, age) => {
console.log("test1", name, age);
});
syncWaterfallHook.tap("test2", (name, age) => {
console.log("test2", name, age);
return "test2";
});
syncWaterfallHook.tap("test3", (name, age) => {
console.log("test3", name, age);
});
syncWaterfallHook.call("szy", "18");
test1 szy 18
test2 szy 18
test3 test2 18
SyncLoopHook
同步顺序执行,知道所有的函数全部返回undefined,否则重头执行
import { SyncLoopHook } from "./lib";
const syncLoopHook = new SyncLoopHook();
let count = 3;
syncLoopHook.tap("test1", () => {
console.log("test1... ", count);
if (count < 3) {
return;
}
return count--;
});
syncLoopHook.tap("test2", () => {
console.log("test2... ", count);
if (count < 2) {
return;
}
return count--;
});
syncLoopHook.tap("test3", () => {
console.log("test3... ", count);
if (count < 1) {
return;
}
return count--;
});
syncLoopHook.call();
test1... 3
test1... 2
test2... 2
test1... 1
test2... 1
test3... 1
test1... 0
test2... 0
test3... 0
AsyncParallelHook(回调形式)
特点:异步,并行
- 使用tapAsync注册,callAsync触发
- 注册时需要传入cb回调,代表函数完成
- callAsync的回调
- 如果所有的cb全是undefined,将在注册函数完成后执行
- 如果其中有一个cb传入error,所有的注册函数因并行还是会全部执行,但是callAsync的回调会在第一个cb(err)时结束
import { AsyncParallelHook } from "./lib";
const asyncParallelHook = new AsyncParallelHook(["name", "age"]);
asyncParallelHook.tapAsync("test1", (name, age, cb) => {
console.log("test1入口");
setTimeout(() => {
console.log("test1", name, age);
cb();
// cb({ success: false, name: "test1" });
}, 2000);
});
asyncParallelHook.tapAsync("test2", (name, age, cb) => {
console.log("test2入口");
setTimeout(() => {
console.log("test2", name, age);
cb();
// cb({ success: false, name: "test2" });
}, 3000);
});
asyncParallelHook.tapAsync("test3", (name, age, cb) => {
console.log("test3入口");
setTimeout(() => {
console.log("test3", name, age);
cb();
}, 1000);
});
asyncParallelHook.callAsync("szy", "18", data => {
console.log("end", data);
});
test1入口
test2入口
test3入口
test3 szy 18
test1 szy 18
test2 szy 18
end undefined
test1入口
test2入口
test3入口
test3 szy 18
test1 szy 18
end { success: false, name: 'test1' }
test2 szy 18
AsyncSeriesHook(回调形式)
特点:异步,串行
- 使用tapAsync注册,callAsync触发
- 注册时需要传入cb回调,代表函数完成
- callAsync的回调
- 如果所有的cb全是undefined,将在注册函数完成后执行
- 如果其中有一个cb传入error,则后续注册函数不再执行
import { AsyncSeriesHook } from "./lib";
const asyncSeriesHook = new AsyncSeriesHook(["name", "age"]);
asyncSeriesHook.tapAsync("test1", (name, age, cb) => {
console.log("test1入口");
setTimeout(() => {
console.log("test1", name, age);
cb();
}, 2000);
});
asyncSeriesHook.tapAsync("test2", (name, age, cb) => {
console.log("test2入口");
setTimeout(() => {
console.log("test2", name, age);
cb();
// cb({ success: false });
}, 3000);
});
asyncSeriesHook.tapAsync("test3", (name, age, cb) => {
console.log("test3入口");
setTimeout(() => {
console.log("test3", name, age);
cb();
}, 1000);
});
asyncSeriesHook.callAsync("szy", "18", err => {
console.log("end", err);
});
test1入口
test1 szy 18
test2入口
test2 szy 18
test3入口
test3 szy 18
end undefined
test1入口
test1 szy 18
test2入口
test2 szy 18
end { success: false }
AsyncSeriesBailHook(Promise)
特点:异步,串行,保险类型
- 使用tapPromise注册,promise触发
- 如果遇到resolve有返回值时,退出当前任务
import { AsyncSeriesBailHook } from "./lib";
const asyncSeriesBailHook = new AsyncSeriesBailHook(["name", "age"]);
asyncSeriesBailHook.tapPromise("test1", (name, age) => {
console.log("test1入口");
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log("test1", name, age);
resolve();
}, 2000);
});
});
asyncSeriesBailHook.tapPromise("test2", (name, age) => {
console.log("test2入口");
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log("test2", name, age);
resolve({ success: true });
}, 3000);
});
});
asyncSeriesBailHook.tapPromise("test3", (name, age, cb) => {
console.log("test3入口");
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log("test3", name, age);
resolve();
}, 1000);
});
});
asyncSeriesBailHook.promise("szy", "18").then(res => {
console.log(res);
});
test1入口
test1 szy 18
test2入口
test2 szy 18
{ success: true }
其他Hook
以上总结了同步、异步,已经瀑布、保险、循环类型的hook调用示例。
AsyncParallelBailHookAsyncSeriesWaterfallHookAsyncSeriesLoopHook
剩下的这几个函数也可以参照上述栗子🌰,自己实现下~
Before/Stage
在tap注册函数时,第一个参数可以是一个对象,name是注册名,before和stage决定了所有注册函数的顺序
Before
import { SyncHook } from "./lib";
const syncHook = new SyncHook(["name", "age"]);
syncHook.tap(
{
name: "test1"
},
(name, age) => {
console.log("test1", name, age);
}
);
// 将test2提前于test1
syncHook.tap(
{
name: "test2",
before: "test1"
},
(name, age) => {
console.log("test2", name, age);
}
);
syncHook.tap(
{
name: "test3"
},
(name, age) => {
console.log("test3", name, age);
}
);
syncHook.call("szy", "18");
test2 szy 18
test1 szy 18
test3 szy 18
Stage
import { SyncHook } from "./lib";
const syncHook = new SyncHook(["name", "age"]);
syncHook.tap(
{
name: "test1",
stage: 0
},
(name, age) => {
console.log("test1", name, age);
}
);
syncHook.tap(
{
name: "test2",
stage: 1
},
(name, age) => {
console.log("test2", name, age);
}
);
syncHook.tap(
{
name: "test3",
stage: -1
},
(name, age) => {
console.log("test3", name, age);
}
);
syncHook.call("szy", "18");
test3 szy 18
test1 szy 18
test2 szy 18
stage默认值是0,支持负数,从小到大执行
⚠️ 如果同时设置before和stage,会优先执行before逻辑,也就是说before优先级更高。
Hook拦截器
下面我们看一个loop典型函数的拦截过程
import { SyncLoopHook } from "./lib";
const syncLoopHook = new SyncLoopHook();
syncLoopHook.intercept({
// register: tap函数注册完触发
register: tap => {
console.log(tap.name + "register拦截器");
return tap;
},
// call: call函数执行后触发
call: (...args) => {
console.log("call拦截器");
},
// tap: tap中回调函数前触发
tap: tap => {
console.log("tap拦截器");
},
// loop: loop类型函数在回调前触发
loop: (...args) => {
console.log("loop拦截器");
}
});
// 省略syncLoopHook.tap的注册过程
syncLoopHook.call();
HookMap
HookMap本质上是一个辅助类,提供了for、get等实例方法,我们可以通过HookMap更好的分类管理
还是SyncHook这个栗子:
import { HookMap, SyncHook } from "./lib";
const hookMap = new HookMap(key => {
console.log(key);
return new SyncHook(["name", "age"]);
});
hookMap.for("key1").tap("test1", (name, age) => {
console.log("key1: test1", name, age);
});
hookMap.for("key2").tap("test2-1", (name, age) => {
console.log("key2: test2-1", name, age);
});
hookMap.for("key2").tap("test2-2", (name, age) => {
console.log("key2: test2-2", name, age);
});
hookMap.for("key3").tap("test3", (name, age) => {
console.log("key3: test3", name, age);
});
const hook1 = hookMap.get("key2");
hook1.call("szy", 18);
key1
key2
key3
key2: test2-1 szy 18
key2: test2-2 szy 18
- 可以看到HookMap接受一个函数,参数是for方法存入的key值,返回一个tapable hook。
- 通过HookMap的实例方法for去创建一个分组(key2),等到一个Hook类的实例。
- 给实例添加tap、tapAsync等方法。
- 通过HookMap的实例方法get获取当前分组(key2)下的Hook对象。
- 调用Hook对象...
MultiHook
MultiHook主要就是将多个hook整合在一起,可以批量拦截和注册(tap/tapAsync/tapPromise)。
this.hooks.allHooks = new MultiHook([this.hooks.hookA, this.hooks.hookB]);
实现原理
下面以SyncHook函数讲解下tapable的主要执行过程
创建Hook实例
我们看下new SyncHook的构建过程。
const syncHook = new SyncHook(["name", "age"]);
function SyncHook(args = [], name = undefined) {
const hook = new Hook(args, name);
hook.constructor = SyncHook;
hook.tapAsync = TAP_ASYNC; // 异步函数抛出错误
hook.tapPromise = TAP_PROMISE; // Promise函数抛出错误
hook.compile = COMPILE;
return hook;
}
Hook类主要创建了taps数组(所有tap事件)、参数name、age的收集、所有的拦截器,还有就是注册和触发的事件(tap、call...),至于这个_x我们之后会说到。
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;
}
...
}
接下来就是compile这个抽象类,他会由继承Hook的子类实现,比如SyncHook就是这么实现的:
const COMPILE = function(options) {
factory.setup(this, options);
return factory.create(options);
};
这边的实现过程可以略过,后面会讲到😜
tap注册事件
接下来我们一起探索下tap事件的注册过程
syncHook.tap("test1", (name, age) => {
console.log("test1", name, age);
});
首先进入Hook类中的_tap函数
将同步函数类型,test1,处理的回调函数传入
// 注明同步函数类型
this._tap("sync", options, fn);
_tap(type, options, fn) {
...
// 一些options的整合处理
options = Object.assign({ type, fn }, options);
// 执行register拦截器
options = this._runRegisterInterceptors(options);
this._insert(options);
}
- _tap函数内部将options整合,以及执行了所有拦截器中的register。
- _insert函数会根据tap注册事件时我们传入的before和stage,对所有的事件进行排序,插入Hook类的taps数组。
call函数调用
到了最后的调用函数
syncHook.call("szy", "18");
- 会进入到_createCall,声明同步类型
this.call = this._createCall("sync");
- 进入上文的SyncHook自己的COMPILE
setup(instance, options) {
instance._x = options.taps.map(t => t.fn);
}
// 动态创建call函数
factory.create(options);
- 将所有的taps数组中存的事件放入实例的_x属性中(此时taps的执行已经排过序)
- 执行create方法
create
这边就用到了tapable中的另一个核心类,HookCodeFactory工厂函数。我们来看看create的实现
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
})
);
这边使用了new Function来动态创建了call的执行函数,(用Function的主要原因是因为性能方面的考虑,在大量使用的场景下性能会比较好,具体可以参考github.com/webpack/tap…)。
具体步骤如下:
- 获取调用参数(args数组),赋值给Function的参数列表
- header函数对内部的_x函数列表和拦截器赋值
- contentWithInterceptors函数
-
- 执行call拦截器
- 遍历taps数组,以下标获取_x列表中对应的函数
- 将args参数列表的形参带入_x函数的实参(对应了第一步的Function参数)
最终生成了什么❓
(function anonymous(name, age
) {
"use strict";
var _context;
var _x = this._x;
var _fn0 = _x[0];
_fn0(name, age);
var _fn1 = _x[1];
_fn1(name, age);
var _fn2 = _x[2];
_fn2(name, age);
})
总结
- tapable中除了同步执行函数,也提供Waterfall、Bail、Loop类型的函数,在支持同步的基础上添加了异步的串行与并行函数
- tapable也提供了一系列的辅助函数、拦截器等功能。
- 通过对SyncHook函数的源码解析,对tapable内部实现得到清晰的了解。
参考链接
转载自:https://juejin.cn/post/7217381222024413240