5-3 扩展手撸SyncHook&AsyncHook
目标
理解 tapable 的内部实现原理,通过手写简化版的 SyncHook 和 AsyncSeriesHook 来深入理解其工作机制。
手写 SyncHook
SyncHook 的核心逻辑:维护一个回调数组,按注册顺序依次执行。
class SyncHook {
constructor(args = []) {
this.args = args; // 参数名列表
this.taps = []; // 注册的回调
this.interceptors = []; // 拦截器
}
// 注册回调
tap(name, fn) {
this.taps.push({ name, fn });
}
// 触发执行
call(...args) {
// 拦截器: call 阶段
for (const interceptor of this.interceptors) {
if (interceptor.call) interceptor.call(...args);
}
// 按顺序执行所有回调
for (const tap of this.taps) {
// 拦截器: tap 阶段
for (const interceptor of this.interceptors) {
if (interceptor.tap) interceptor.tap(tap);
}
tap.fn(...args);
}
}
// 注册拦截器
intercept(interceptor) {
this.interceptors.push(interceptor);
}
}
// 使用示例
const hook = new SyncHook(['name', 'age']);
hook.tap('Logger', (name, age) => {
console.log(`[Logger] ${name} is ${age} years old`);
});
hook.tap('Validator', (name, age) => {
if (age < 0) console.log('Invalid age!');
});
hook.call('Alice', 25);
// [Logger] Alice is 25 years old
javascript
手写 SyncBailHook
SyncBailHook 在 SyncHook 基础上增加了中断逻辑:如果回调返回非 undefined 值,停止执行后续回调。
class SyncBailHook {
constructor(args = []) {
this.args = args;
this.taps = [];
}
tap(name, fn) {
this.taps.push({ name, fn });
}
call(...args) {
for (const tap of this.taps) {
const result = tap.fn(...args);
// 如果返回非 undefined,立即停止并返回该值
if (result !== undefined) {
return result;
}
}
return undefined;
}
}
// 使用示例
const hook = new SyncBailHook(['value']);
hook.tap('Check1', (value) => {
if (value < 0) return 'negative';
return undefined; // 继续执行
});
hook.tap('Check2', (value) => {
console.log('Value is positive:', value);
});
console.log(hook.call(-5)); // 'negative'(Check2 不会执行)
console.log(hook.call(10)); // undefined, 输出 "Value is positive: 10"
javascript
手写 SyncWaterfallHook
WaterfallHook 将上一个回调的返回值传给下一个回调。
class SyncWaterfallHook {
constructor(args = []) {
this.args = args;
this.taps = [];
}
tap(name, fn) {
this.taps.push({ name, fn });
}
call(...args) {
let result = args[0]; // 初始值
for (const tap of this.taps) {
result = tap.fn(result);
// 如果返回 undefined,保持上一个值
if (result === undefined) {
// 保持当前 result 不变
}
}
return result;
}
}
// 使用示例
const hook = new SyncWaterfallHook(['value']);
hook.tap('Double', (value) => value * 2);
hook.tap('AddTen', (value) => value + 10);
console.log(hook.call(5)); // 20 (5*2=10, 10+10=20)
javascript
手写 AsyncSeriesHook
AsyncSeriesHook 通过回调函数(callback)实现异步串行执行。
class AsyncSeriesHook {
constructor(args = []) {
this.args = args;
this.taps = [];
}
tapAsync(name, fn) {
this.taps.push({ name, fn });
}
tapPromise(name, fn) {
this.taps.push({ name, fn });
}
callAsync(...args) {
const finalCallback = args.pop(); // 最后一个参数是完成回调
let index = 0;
const next = (err) => {
if (err) {
finalCallback(err);
return;
}
if (index >= this.taps.length) {
finalCallback(null);
return;
}
const tap = this.taps[index++];
if (tap.fn.length > args.length) {
// 异步回调风格
tap.fn(...args, next);
} else {
// 同步风格
tap.fn(...args);
next();
}
};
next();
}
promise(...args) {
return new Promise((resolve, reject) => {
this.callAsync(...args, (err) => {
if (err) reject(err);
else resolve();
});
});
}
}
// 使用示例
const hook = new AsyncSeriesHook(['url']);
hook.tapAsync('FetchAPI', (url, callback) => {
console.log(`Fetching ${url}...`);
setTimeout(() => {
console.log('Fetch done');
callback();
}, 1000);
});
hook.tapAsync('ParseData', (url, callback) => {
console.log('Parsing data...');
setTimeout(() => {
console.log('Parse done');
callback();
}, 500);
});
hook.callAsync('https://api.example.com', (err) => {
console.log('All tasks completed');
});
javascript
手写 AsyncParallelHook
AsyncParallelHook 同时执行所有异步回调,全部完成后触发最终回调。
class AsyncParallelHook {
constructor(args = []) {
this.args = args;
this.taps = [];
}
tapAsync(name, fn) {
this.taps.push({ name, fn });
}
callAsync(...args) {
const finalCallback = args.pop();
let remaining = this.taps.length;
let hasError = false;
if (remaining === 0) {
finalCallback(null);
return;
}
for (const tap of this.taps) {
tap.fn(...args, (err) => {
if (hasError) return;
if (err) {
hasError = true;
finalCallback(err);
return;
}
remaining--;
if (remaining === 0) {
finalCallback(null);
}
});
}
}
}
// 使用示例
const hook = new AsyncParallelHook(['data']);
hook.tapAsync('TaskA', (data, cb) => {
setTimeout(() => { console.log('A done'); cb(); }, 1000);
});
hook.tapAsync('TaskB', (data, cb) => {
setTimeout(() => { console.log('B done'); cb(); }, 500);
});
hook.callAsync({}, () => {
console.log('All parallel tasks done');
});
// B done (500ms) → A done (1000ms) → All parallel tasks done
javascript
实现要点总结
| Hook 类型 | 核心逻辑 | 关键区别 |
|---|---|---|
| SyncHook | 遍历数组依次执行 | 无特殊处理 |
| SyncBailHook | 检查返回值,非 undefined 则中断 | 中断机制 |
| SyncWaterfallHook | 传递上一个返回值给下一个 | 数据流动 |
| AsyncSeriesHook | 递归调用 next() 实现串行 | 异步串行 |
| AsyncParallelHook | 计数器,全部完成时回调 | 异步并行 |
实际的 tapable 源码要复杂得多——它使用了代码生成(new Function)来优化执行性能,而不是简单的循环。但这种简化实现足以理解其核心思想。
参考资源
- tapable 源码 - 官方实现
- Webpack 插件开发
↑