tapable
- webpack基于tapable实现了事件流
- 自定义plugin的时候,语法与tapable一致
语法
- new
tapable的某个基类
,传入固定长度的array
- 在实例上使用tap或tapAsync注册事件:
- 第一个参数为name,给自己看的,内部不关心这个参数
- callback,回调函数,接收参数可以小于等于初始化的array长度,超出了拿不到值,哪怕触发的时候传递了
- call派发事件,事件依次执行,可以传递参数,执行顺序为
先进先出
基类
- Async开头为异步
- 注册的话支持tap、tapAsync、tapPromise
- 派发方法没有call,只有callAsync
- Sync开头为同步
按照值的返回分类
Basic
- 依次执行每一个事件函数,不关心返回值
- SyncHook、AsyncParallelHook、AsyncSeriesHook
Bail
- 依次执行事件,当遇到返回值不为undefined的函数,后续函数不再执行
- SyncBailHook、AsyncSeriesBailHook, AsyncParallelBailHook
Waterfall
- 如果前一个事件返回不为undefined,那么将前一个事件的结果当做后一个事件的参数
- SyncWaterfallHook,AsyncSeriesWaterfallHook
Loop
- 不停的执行函数,知道所有函数返回为undefined(
也可理解为没有返回值
)
- 遇到返回值不为undefined的函数,执行完当前函数,跳过后续函数,再次从起始位置依次执行函数
- SyncLoopHook 和 AsyncSeriesLoopHook
SyncHook
const { SyncHook } = require("tapable");
const hook = new SyncHook(["name", "age"]);
hook.tap("1", (name, age, cc) => {
console.log(1, name, age, cc);
});
hook.tap("2", (name, age) => {
console.log(2, name, age);
});
hook.call("haha", 10);
SyncBailHook
- 依次执行当前事件
- 如果遇到函数有返回值,那么跳过后续函数
- 如果执行完毕也没遇到返回值,也停止执行,不执行第二轮
const { SyncBailHook } = require("tapable");
const hook = new SyncBailHook(["name", "age"]);
hook.tap("1", (name, age) => {
console.log(1, name, age);
return 1;
});
hook.tap("2", (name, age) => {
console.log(2, name, age);
});
hook.call("haha", 10);
SyncLoopHooks
- 当所有事件都返回undefind结束执行,否则重复执行
- 如果当前函数返回值不为undefined,那么跳过后续函数,从头开始执行
- 如果函数内部没有做好判断,会导致死循环
const { SyncLoopHook } = require("tapable");
const hook = new SyncLoopHook();
hook.tap("1", () => {
console.log(1);
});
hook.tap("2", () => {
console.log(2);
return 1;
});
hook.tap("3", () => {
console.log(3);
});
hook.call();
SyncWaterfallHook
- 如果前面函数的值为undefined,那么使用以前的值
- 否则使用前一个函数的返回值
- 当出现不为undefined的返回值时,可以理解为全局的name就被覆盖了,那么依次执行的时候,会以最新的为准
const { SyncWaterfallHook } = require("tapable");
const hook = new SyncWaterfallHook(["name"]);
hook.tap("1", (name) => {
console.log(1, name);
return 1;
});
hook.tap("2", (name) => {
console.log(2, name);
});
hook.tap("3", (name) => {
console.log(3, name);
});
hook.call("hh");
AsyncParallelHook
- 异步触发
- 依然是自上而下执行,结束会执行callAsync的callback
const { AsyncParallelHook } = require("tapable");
const hook = new AsyncParallelHook(["name", "age"]);
const { AsyncParallelHook } = require("tapable");
const hook = new AsyncParallelHook(["name", "age"]);
hook.tap("1", (name, age) => {
console.log(1, name, age);
});
hook.tap("2", (name, age) => {
console.log(2, name, age);
});
hook.tap("3", (name, age) => {
console.log(3, name, age);
});
hook.tapAsync("4", (name, age, callback) => {
console.log(4, name, age);
callback();
});
hook.tapAsync("5", (name, age, callback) => {
console.log(5, name, age);
callback();
});
hook.tapPromise("6", (name, age) => {
return new Promise((res, rej) => {
console.log(6, name, age);
res();
});
});
hook.callAsync("cc", 11, () => {
console.log("结束");
});
hook.callAsync("cc", 11, () => {
console.log("结束");
});
AsyncParallelBailHook
const { AsyncParallelBailHook } = require("tapable");
const hook = new AsyncParallelBailHook();
hook.tapPromise("1", () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(1);
}, 1000);
});
});
hook.tapPromise("2", () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
});
});
});
hook.promise().then((res) => {
console.log(res, 999);
});
AsyncSeriesHook
const { AsyncSeriesHook } = require("tapable");
const hook = new AsyncSeriesHook();
hook.tapAsync("1", (callback) => {
console.log(1);
callback();
});
hook.tapAsync("2", (callback) => {
console.log(2);
callback();
});
hook.callAsync((...arg) => {
console.log("奥利给", arg);
});
AsyncSeriesBailHook
const { AsyncSeriesBailHook } = require("tapable");
const hook = new AsyncSeriesBailHook();
hook.tapAsync("1", (callback) => {
console.log(1);
callback(1);
});
hook.tapAsync("2", (callback) => {
console.log(2);
callback();
});
hook.callAsync((...arg) => {
console.log("奥利给", arg);
});
事件的注册与派发
注册
tap
tapAsync
- tapAsync为异步注册
- 会额外给事件传递一个参数,callback
- 执行callback会来标识当前回调执行完毕
const { AsyncParallelHook } = require("tapable");
const hook = new AsyncParallelHook(["name", "age"]);
hook.tapAsync("1", (name, age, callback) => {
console.log(1, name, age);
callback();
});
hook.callAsync("cc", 11, () => {
console.log("结束");
});
tapPromise
派发
call
callAsync
- 异步派发,可多传递一个callback,当所有函数执行完毕,会执行callback
- 前提是函数告知我们,如tapAsync中执行callback,promise执行resolve
- 给callback传参会立即结束后续执行
promise
- 就是promise,可以理解为promise.all
hook
.promise("cc", 11)
.then((res) => {
console.log("啥呀");
})
.catch((err) => {
console.log(err);
})
.finally(() => {
console.log(".....");
});
拦截器 intercept
- intercept接收一个object,每当对应的钩子触发,会进行拦截,可以有多个intercept
- call
- tap
- 并非是tap执行,而是tap注册的事件被执行前触发
- register
- 注册事件的时候会执行:tap、tapAsync、tapPromise
const { SyncHook } = require("tapable");
const hook = new SyncHook(["name", "age"]);
hook.intercept({
register(tapInfo) {
console.log("register拦截器1", tapInfo);
},
call(name, age) {
console.log("call拦截器1", name, age);
},
tap(tapInfo) {
console.log("tap拦截器1", tapInfo);
},
});
hook.intercept({
register(tapInfo) {
console.log("register拦截器2", tapInfo);
},
call(name, age) {
console.log("call拦截器2", name, age);
},
tap(tapInfo) {
console.log("tap拦截器2", tapInfo);
},
});
hook.tap("1", (name, age) => {
console.log("回调1", name, age);
});
hook.call("cc", 11);
stage
- 设置权重,某些事件优先级比较高,但设置的时候相对靠后,我们希望它按照给定顺序执行
- stage写在注册是第一个参数中:
- 数值越大,级别越高,越先执行
- 同级别按照注册时间执行
- 默认为0
- 在注册事件的时候,会按照stage进行插入,而不是call的时候排序
const { SyncHook } = require("tapable");
const hook = new SyncHook(["name", "age"]);
hook.tap({ name: "tap1", stage: 2 }, () => {
console.log("tap2");
});
hook.tap({ name: "tap1", stage: 1 }, () => {
console.log("tap1");
});
hook.tap("tap1", (name, age) => {
console.log(1, name, age);
});
hook.call("cc", 11);
before
- 以名字来匹配优先级,高于stage
- 可以指定在一个或多个前面执行
- 多个的话,会以最前面的执行
- 如果指定的项不存在,那么会直接到第一项
- 同样是在注册的时候进行排序
const { SyncHook } = require("tapable");
const hook = new SyncHook();
hook.tap("tap1", () => {
console.log("tap1");
});
hook.tap("tap2", () => {
console.log("tap2");
});
hook.tap({ name: "tap3" }, () => {
console.log("tap3");
});
hook.tap({ name: "tap4", before: ["tap3"] }, () => {
console.log("tap4");
});
hook.tap({ name: "tap5", before: ["tap2", "tap6"] }, () => {
console.log("tap5");
});
hook.call();
HookMap
- 通过HookMap将基类进行分组
工厂模式
- for方法设置一个基类
- get方法获取一个基类
- 触发还是call,设置还是tap
- 原理为初始化的时候,保存callback
- for的本质还是get,但是会加一层初始化逻辑,如果key不存在就执行callback初始化
const { SyncHook, HookMap } = require("tapable");
const map = new HookMap(() => new SyncHook(["name"]));
map.for("key1").tap("k1-p1", (name) => {
console.log("k1-p1", name);
});
map.for("key1").tap("k1-p2", (name) => {
console.log("k1-p2", name);
});
map.for("key2").tap("k2-p1", (name) => {
console.log("k2-p1", name);
});
map.for("key3").tap("k3-p1", (name) => {
console.log("k3-p1", name);
});
map.get("key1").call("name");
map.get("key2").call("name");
map.get("key3").call("name");
实现
- 首先这么多类都是基于Hook的,在Hook上方了一些公有方法,可以继承
- 一些私有方法写在私有类本身,因为this指向实例,可以向上查找,所以完全没问题
- 整体使用的是一个懒编译模式,不调用派发不生成函数,以call举例,并非是for循环执行,而是通过new Function生成了一个新的函数,在里面依次执行
- 如果不调用call的话,不加载函数
- 如果调用的话,生成一次后覆盖call,那么后续执行call的话,执行的就是前面生成的fn,不用走编译阶段了
- 如果调用后,又进行事件的注册,那么会先清除上次的函数,后续调用call会重新生成,避免注册后不执行问题
index.js
const SyncHook = require("./SyncHook");
const AsyncParallelHook = require("./AsyncParallelHook");
const HookMap = require("./HookMap");
module.exports = {
SyncHook,
AsyncParallelHook,
HookMap,
};
Hook
const CALL_DELEGATE = function (...args) {
this.call = this._createCall("sync");
return this.call(...args);
};
const CALL_ASYNC_DELEGATE = function (...args) {
this.callAsync = this._createCall("async");
return this.callAsync(...args);
};
const PROMISE_DELEGATE = function (...args) {
this.promise = this._createCall("promise");
return this.promise(...args);
};
class Hook {
constructor(args) {
this.args = Array.isArray(args) ? args : [];
this.taps = [];
this.call = CALL_DELEGATE;
this.callAsync = CALL_ASYNC_DELEGATE;
this.promise = PROMISE_DELEGATE;
this._x;
this.interceptors = [];
}
intercept(interceptor) {
this.interceptors.push(interceptor);
}
tap(options, fn) {
this._tap("sync", options, fn);
}
tapAsync(options, fn) {
this._tap("async", options, fn);
}
tapPromise(options, fn) {
this._tap("promise", options, fn);
}
_tap(type, options, fn) {
if (typeof options === "string") {
options = { name: options };
}
const tapInfo = { ...options, type, fn };
this.runRegisterInterceptors(tapInfo);
this._insert(tapInfo);
}
runRegisterInterceptors(tapInfo) {
for (const interceptor of this.interceptors) {
if (interceptor.register) {
interceptor.register(tapInfo);
}
}
}
_insert(tapInfo) {
this.resetCompilation();
let before;
if (typeof tapInfo.before === "string") {
before = new Set([tapInfo.before]);
} else if (Array.isArray(tapInfo.before)) {
before = new Set(tapInfo.before);
}
let stage = 0;
if (typeof tapInfo.stage === "number") {
stage = tapInfo.stage;
}
let i = this.taps.length;
while (i > 0) {
i--;
const x = this.taps[i];
this.taps[i + 1] = x;
if (before) {
if (before.has(x.name)) {
before.delete(x.name);
continue;
}
if (before.size > 0) {
continue;
}
}
const xStage = x.stage || 0;
if (xStage > stage) {
continue;
}
i++;
break;
}
this.taps[i] = tapInfo;
}
resetCompilation() {
this.call = CALL_DELEGATE;
this.callAsync = CALL_ASYNC_DELEGATE;
this.promise = PROMISE_DELEGATE;
}
_createCall(type) {
return this.compile({
taps: this.taps,
args: this.args,
interceptors: this.interceptors,
type,
});
}
}
module.exports = Hook;
HookCodeFactory
class HookCodeFactory {
setup(hookInstance, options) {
hookInstance._x = options.taps.map((tapInfo) => tapInfo.fn);
}
init(options) {
this.options = options;
}
args(config = {}) {
const { before, after } = config;
let allArgs = this.options.args || [];
allArgs = [...allArgs];
if (before) {
allArgs.unshift(before);
}
if (after) {
allArgs.push(after);
}
if (allArgs.length > 0) {
return allArgs.join(",");
}
}
header() {
let code = ``;
code += `var _x = this._x;\n`;
const interceptors = this.options.interceptors;
if (interceptors.length > 0) {
code += `var _taps = this.taps;\n`;
code += `var _interceptors = this.interceptors;\n`;
interceptors.forEach((interceptor, i) => {
if (interceptor.call) {
code += `_interceptors[${i}].call(${this.args()});\n`;
}
});
}
return code;
}
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(
this.args({ after: "_callback" }),
this.header() + this.content({ onDone: () => "_callback();\n" })
);
break;
case "promise":
let tapsContent = this.content({ onDone: () => "_resolve();\n" });
let content = `return new Promise(function (_resolve, _reject){
${tapsContent}
})`;
fn = new Function(this.args(), this.header() + content);
break;
}
this.deinit();
return fn;
}
deinit() {
this.options = null;
}
callTapsSeries() {
let code = "";
for (let j = 0; j < this.options.taps.length; j++) {
const tapContent = this.callTap(j, {});
code += tapContent;
}
return code;
}
callTapsParallel({ onDone }) {
let code = ``;
const taps = this.options.taps;
code += `var _counter = ${taps.length};\n`;
code += `
var _done = function() {
${onDone()};
};\n
`;
for (let i = 0; i < taps.length; i++) {
const tapContent = this.callTap(i, {
onDone: () => `if(--_counter === 0)_done();`,
});
code += tapContent;
}
return code;
}
callTap(tapIndex, { onDone }) {
let code = ``;
const interceptors = this.options.interceptors;
if (interceptors.length > 0) {
code += `var _tap${tapIndex} = _taps[${tapIndex}];\n`;
interceptors.forEach((interceptor, index) => {
if (interceptor.tap) {
code += `_interceptors[${index}].tap(_tap${tapIndex});\n`;
}
});
}
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 += onDone();
break;
case "async":
code += `_fn${tapIndex}(${this.args()}, function() {
if (--_counter === 0) _done();
});\n`;
break;
case "promise":
code += `
var _promise${tapIndex} = _fn${tapIndex}(${this.args()});
_promise${tapIndex}.then(function(){
if(--_counter === 0) _done();
}).catch(err=>{
_reject(err);
})
`;
break;
}
return code;
}
}
module.exports = HookCodeFactory;
AsyncParallelHook
const Hook = require("./Hook");
const HookCodeFactory = require("./HookCodeFactory");
class AsyncParallelHookCodeFactory extends HookCodeFactory {
content({ onDone }) {
return this.callTapsParallel({ onDone });
}
}
const factory = new AsyncParallelHookCodeFactory();
class AsyncParallelHook extends Hook {
compile(options) {
factory.setup(this, options);
return factory.create(options);
}
}
module.exports = AsyncParallelHook;
SyncHook
const Hook = require("./Hook");
const HookCodeFactory = require("./HookCodeFactory");
class SyncHookCodeFactory extends HookCodeFactory {
content() {
return this.callTapsSeries();
}
}
const factory = new SyncHookCodeFactory();
class SyncHook extends Hook {
compile(options) {
factory.setup(this, options);
return factory.create(options);
}
}
module.exports = SyncHook;
HookMap
class HookMap {
constructor(factory) {
this.factory = factory;
this.map = new Map();
}
get(key) {
return this.map.get(key);
}
for(key) {
const hook = this.get(key);
if (hook) return hook;
let newHook = this.factory();
this.map.set(key, newHook);
return newHook;
}
}
module.exports = HookMap;