tapable是一种事件驱动型事件流机制,是一个类似于 Node.js 中的 EventEmitter的库,但更专注于自定义事件的触发和处理。webpack 通过 tapable 将实现与流程解耦,所有具体实现通过插件的形式存在。,其工作流程为
1.实例化Hook注册事件监听
2.通过Hook触发事件监听
3.执行懒编译生成的可执行代码
Hook的执行机制可分为同步和异步两种,其中异步又分为并行和串行两种模;Hook的钩子类型分为四种:
- Hook:普通钩子,监听器之间互相独立不干扰
- BailHook:熔断钩子,某个监听返回非undefined时后续不执行
- WaterfallHook:瀑布钩子,上一个监听的返回值可传递至下一个
- LoopHook:循环钩子,如果当前返回非undefined则一直执行(从头开始执行)(webpack中不常见)
同步钩子
通过tap注册钩子事件,通过call执行钩子事件
SyncHook
每个事件独立执行,互相不影响
const { SyncHook } = require('tapable')
const hook = new SyncHook(['name', 'age'])
hook.tap('fn1', (name, age) => {
console.log(`fn1 => ${name}: ${age}`)
})
hook.tap('fn2', (name, age) => {
console.log(`fn2 => ${name}: ${age}`)
})
hook.call('yhzzy', 18) // fn1 => yhzzy: 18 fn2 => yhzzy: 18
SyncBailHook
某个监听返回非undefined时后续不执行
const { SyncBailHook } = require('tapable')
const hook = new SyncBailHook(['name', 'age'])
hook.tap('fn1', (name, age) => {
console.log(`fn1 => ${name}: ${age}`)
})
hook.tap('fn2', (name, age) => {
console.log(`fn2 => ${name}: ${age}`)
return true
})
hook.tap('fn3', (name, age) => {
console.log(`fn3 => ${name}: ${age}`)
})
hook.call('yhzzy', 18) // fn1 => yhzzy: 18 fn2 => yhzzy: 18
SyncWaterfallHook
上一个监听的返回值可传递至下一个,覆盖原来的参数值
const { SyncWaterfallHook } = require('tapable')
const hook = new SyncWaterfallHook(['name', 'age'])
hook.tap('fn1', (name, age) => {
console.log(`fn1 => ${name}: ${age}`)
return 'tom'
})
hook.tap('fn2', (name, age) => {
console.log(`fn2 => ${name}: ${age}`)
return 'mike'
})
hook.tap('fn3', (name, age) => {
console.log(`fn3 => ${name}: ${age}`)
return 'jim'
})
hook.call('yhzzy', 18) // fn1 => yhzzy: 18 fn2 => tom: 18 fn3 => mike: 18
SyncLoopHook
- 执行时,如果都没有返回,则执行一遍后就退出
- 如果有一个返回了非undefined,则从头重新开始执行
- 当遇到返回了undefined则执行完所有监听后退出
const { SyncLoopHook } = require('tapable')
const hook = new SyncLoopHook(['name', 'age'])
let count = 0
hook.tap('fn1', (name, age) => {
console.log(`fn1 => ${name}: ${age}`)
})
hook.tap('fn2', (name, age) => {
console.log(`fn2 => ${name}: ${age}`)
if (++count === 2) {
count = 0
return undefined
}
return true
})
hook.tap('fn3', (name, age) => {
console.log(`fn3 => ${name}: ${age}`)
})
hook.call('yhzzy', 18) // fn1 => yhzzy: 18 fn2 => yhzzy: 18 fn1 => yhzzy: 18 fn2 => yhzzy: 18 fn3 => yhzzy: 18
异步钩子
异步钩子可以使用tapPromise注册=>promise调用,或者使用tapAsync注册=>callAsync调用,每种异步钩子的作用和同步一样,只是每个监听是异步返回
异步串行钩子
异步按顺序执行
AsyncSeriesHook
const { AsyncSeriesHook } = require('tapable')
let hook = new AsyncSeriesHook(['name'])
console.time('time')
hook.tapPromise('fn1', function (name) {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log(`fn1 => ${name}`) // fn1 => yhzzy
resolve()
}, 1000)
})
})
hook.tapPromise('fn2', function (name) {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log(`fn2 => ${name}`) // fn2 => yhzzy
resolve()
}, 2000)
})
})
hook.promise('yhzzy').then(function () {
console.timeEnd('time') // time: 3.018s
})
AsyncSeriesBailHook
const { AsyncSeriesBailHook } = require('tapable')
let hook = new AsyncSeriesBailHook(['name'])
console.time('time')
hook.tapPromise('fn1', function (name) {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log(`fn1 => ${name}`) // fn1 => yhzzy
resolve()
}, 1000)
})
})
hook.tapPromise('fn2', function (name) {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log(`fn2 => ${name}`) // fn2 => yhzzy
resolve('hook end')
}, 2000)
})
})
hook.tapPromise('fn3', function (name) {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log(`fn3 => ${name}`) // 被熔断不执行
resolve()
}, 3000)
})
})
hook.promise('yhzzy').then(function (msg) {
console.log(msg) // hook end
console.timeEnd('time') // time: 3.018s
})
AsyncSeriesWaterfallHook
const { AsyncSeriesWaterfallHook } = require('tapable')
let hook = new AsyncSeriesWaterfallHook(['name'])
console.time('time')
hook.tapPromise('fn1', function (name) {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log(`fn1 => ${name}`) // fn1 => yhzzy
resolve('tom')
}, 1000)
})
})
hook.tapPromise('fn2', function (name) {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log(`fn2 => ${name}`) // fn2 => tom
resolve('mike')
}, 2000)
})
})
hook.tapPromise('fn3', function (name) {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log(`fn3 => ${name}`) // fn3 => mike
resolve('jim')
}, 3000)
})
})
hook.promise('yhzzy').then(function (msg) {
console.log(msg) // jim
console.timeEnd('time') // time: 6.029s
})
AsyncSeriesLoopHook
const { AsyncSeriesLoopHook } = require('tapable')
let hook = new AsyncSeriesLoopHook(['name'])
let count = 0
console.time('time')
hook.tapPromise('fn1', function (name) {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log(`fn1 => ${name}`)
resolve()
}, 1000)
})
})
hook.tapPromise('fn2', function (name) {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log(`fn2 => ${name}`)
if (++count === 2) {
count = 0
resolve()
}
resolve('1')
}, 2000)
})
})
hook.tapPromise('fn3', function (name) {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log(`fn3 => ${name}`)
resolve()
}, 3000)
})
})
hook.promise('yhzzy').then(function (msg) {
console.timeEnd('time')
})
// fn1 => yhzzy
// fn2 => yhzzy
// fn1 => yhzzy
// fn2 => yhzzy
// fn3 => yhzzy
// time: 9.028s
异步并行钩子
异步同时执行
AsyncParalleHook
const { AsyncParallelHook } = require('tapable')
const hook = new AsyncParallelHook(['name', 'age'])
console.time('time')
hook.tapAsync('fn1', (name, age, callback) => {
setTimeout(() => {
console.log(`fn1 => ${name}: ${age}`)
callback()
}, 1000)
})
hook.tapAsync('fn2', (name, age, callback) => {
setTimeout(() => {
console.log(`fn2 => ${name}: ${age}`)
callback()
}, 2000)
})
hook.callAsync('yhzzy', 18, function () {
console.timeEnd('time')
})
// fn1 => yhzzy: 18
// fn2 => yhzzy: 18
// time: 2.006s
AsyncParalleBailHook
const { AsyncParallelHook } = require('tapable')
const hook = new AsyncParallelHook(['name', 'age'])
console.time('time')
hook.tapAsync('fn1', (name, age, callback) => {
setTimeout(() => {
console.log(`fn1 => ${name}: ${age}`)
callback()
}, 1000)
})
hook.tapAsync('fn2', (name, age, callback) => {
setTimeout(() => {
console.log(`fn2 => ${name}: ${age}`)
callback('err')
}, 2000)
})
hook.tapAsync('fn3', (name, age, callback) => {
setTimeout(() => {
console.log(`fn3 => ${name}: ${age}`)
callback()
}, 3000)
})
hook.callAsync('yhzzy', 18, function () {
console.timeEnd('time')
})
// fn1 => yhzzy: 18
// fn2 => yhzzy: 18
// time: 2.005s
// fn3 => yhzzy: 18
模拟tapable源码实现
以同步钩子syncHook和异步钩子asyncParallelHook为例
tapable的所有钩子都会继承一个Hook基类,然后通过HookCodeFactory类来生成注册事件时的回调函数,继而执行
基类Hook
Hook是所有钩子都会去继承的父类,我们将源码提炼一下,把主要的环节留下
class Hook {
constructor(args = []) {
this.args = args // 存放参数数组
this.taps = [] // 将来用于存放组装好的tap注册事件对象
this._x = undefined // 将来在HookCodeFactory中会给_x赋值为一个数组,里面包含从taps中遍历提取的每一个注册函数
}
// 注册同步钩子
tap(options, fn) {
// 将注册时传进来的名称转换为键值对形式
if (typeof options === 'string') {
options = { name: options }
}
options = Object.assign({ fn }, options) // { fn:... name:fn1 }
// 调用以下方法将组装好的options添加至taps数组
this._insert(options)
}
// 注册异步钩子
tapAsync(options, fn) {
// 将注册时传进来的名称转换为键值对形式
if (typeof options === 'string') {
options = { name: options }
}
options = Object.assign({ fn }, options) // { fn:... name:fn1 }
// 调用以下方法将组装好的options添加至taps数组
this._insert(options)
}
_insert(options) {
this.taps[this.taps.length] = options
}
// 同步方法执行
call(...args) {
// 创建将来要具体执行的函数代码结构
let callFn = this._createCall('sync')
// 调用上述的函数(args传入进去)
return callFn.apply(this, args)
}
// 异步方法执行
callAsync(...args) {
let callFn = this._createCall('async')
return callFn.apply(this, args)
}
_createCall(type) {
// 调用具体钩子类的compile方法,进行具体执行的函数代码组装
return this.compile({
taps: this.taps,
args: this.args,
type: type
})
}
}
module.exports = Hook
Hook代码组装工厂
class HookCodeFactory {
args({ after, before } = {}) {
let allArgs = this.options.args
if (before) allArgs = [before].concat(allArgs)
if (after) allArgs = allArgs.concat(after)
return allArgs.join(',') // ["name", "age"] => name, age
}
header() {
return `"use strict";var _context;var _x = this._x;`
}
content(type) {
let code = ''
switch (type) {
case 'sync':
for (let i = 0; i < this.options.taps.length; i++) {
code += `var _fn${i} = _x[${i}];_fn${i}(${this.args()});`
}
break
case 'async':
code = `var _counter = ${this.options.taps.length};var _done = (function () {
_callback();
});`
for (let i = 0; i < this.options.taps.length; i++) {
code += `var _fn${i} = _x[${i}];_fn${i}(${this.args()}, (function () {
if (--_counter === 0) _done();
}));`
}
break
}
return code
}
setup(instance, options) { // 先准备后续需要使用到的数据
this.options = options // 这里的操作在源码中是通过 init 方法实现,而我们当前是直接挂在了 this 身上
instance._x = options.taps.map(o => o.fn) // this._x = [f1, f2, ....]
}
create() { // 核心就是创建一段可执行的代码体然后返回
// fn = new Function("name, age", "var _x = this._x, var _fn0 = _x[0]; _fn0(name, age);")
const type = this.options.type
const argsParams = type === 'sync' ? '' : { after: '_callback' }
return new Function(
this.args(argsParams),
this.header() + this.content(this.options.type)
)
}
}
module.exports = HookCodeFactory
具体钩子类
SyncHook.js
const Hook = require('./Hook.js')
const HookCodeFactory = require('./HookCodeFactory.js')
const factory = new HookCodeFactory()
class SyncHook extends Hook {
constructor(args) {
super(args)
}
compile(options) { // {taps: [{}, {}], args: [name, age], type: type}
factory.setup(this, options)
return factory.create(options)
}
}
module.exports = SyncHook
AsyncParallelHook.js
const Hook = require('./Hook.js')
const HookCodeFactory = require('./HookCodeFactory.js')
const factory = new HookCodeFactory()
class AsyncParallelHook extends Hook {
constructor(args) {
super(args)
}
compile(options) { // {taps: [{}, {}], args: [name, age], type: type}
factory.setup(this, options)
return factory.create(options)
}
}
module.exports = AsyncParallelHook