tapable
tapable是一个钩子注册、调用库。tapable包暴露了许多Hook类,可用于为插件创建钩子。基本使用如下:
1 | let { SyncHook } = require('tapable') |
webpack 本身自带的功能比较少,像模块加载、依赖解析等。大部分功能都需要使用插件或者loader去实现。webpack就是使用tapable 来实现插件机制的。
tapable 的几种钩子类型
总的来说tapable的钩子分为同步钩子和异步钩子,细分下又有以下几种钩子。
SyncHook
同步钩子,使用 .tap函数注册钩子,参数1是钩子名,参数2是一个钩子回调函数。当调用 .call 方法时会依次执行所注册的钩子函数,举个例子:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25let { SyncHook } = require('tapable')
class Demo {
constructor(){
this.hooks = {
afterEmit: new SyncHook(['name'])
}
}
tap() {// 注册钩子函数
this.hooks.afterEmit.tap('hook1', function(name) {
console.log('the 1th hook')
})
this.hooks.afterEmit.tap('hook2', function(name) {
console.log('the 2th hook')
})
}
// 调用钩子
start(name) {
this.hooks.afterEmit.call(name)
}
}
let l = new Demo()
l.tap()
l.start('hello world')
在执行上面代码后,可以看到命令行打印会依次打印 the 1th hook、the 2th hook。它的机制很简单,就是将所有的钩子函数保存在了一个数组里,在钩子被调用的时候依次执行。以下是简单实现代码:1
2
3
4
5
6
7
8
9
10
11class SyncHook{
constructor(args){
this.tasks = []
}
call(...args){
this.tasks.forEach((task) => task(...args))
}
tap(evtName, task) {
this.tasks.push(task)
}
}
SyncBailHook
同步保险钩子,和同步钩子唯一的区别是:当钩子函数的返回值非undefined,则不再执行后面的钩子函数。举个例子:1
2
3
4
5
6
7
8
9let hook = new SyncBailHook(['name'])
hook.tap('hook1', function(...args) {
console.log('hook1', ...args)
return '中断执行'
})
hook.tap('hook2', function(...args) {
console.log('hook2', ...args)
})
hook.call('hello world')
因为hook1 返回了一个非undefined值,所以hook2将不会被执行,打印结果将会是 hook1。因为只是在同步钩子的基础上增加了一个保险,所以将同步钩子稍微改写一下就可以实现,源码如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16class SyncBailHook{
constructor(args){
this.tasks = []
}
call(...args){
let ret, index =0;
do {
ret = this.tasks[index++](...args)
} while(index < this.tasks.length && undefined === ret)
}
tap(evtName, task) {
this.tasks.push(task)
}
}
SyncWaterfallHook
同步瀑布流钩子,钩子函数将会被依次执行,上一个钩子函数的返回结果将会被当作参数传给下一个钩子函数;如果上一个钩子函数没有返回值,将使用.call传进来的参数,举个例子:1
2
3
4
5
6
7
8
9let hook = new SyncWaterfallHook(['name'])
hook.tap('hook1', function(...args) {
console.log( ...args)
return '这个值会传给hook2'
})
hook.tap('node', function(...args) {
console.log( ...args)
})
hook.call('i,m hook1')
打印结果将会是 i,m hook1, 这个值会传给hook2。同样的来简单实现一下这个功能
1 | class SyncWaterfallHook{ |
AsyncParallelHook
异步并发钩子,注册的异步钩子将会以并发模式运行。异步钩子居别于同步钩子,需要使用.tapAsync、.callAsync来注册和调用钩子函数,并且.callAsync方法还提供了一个回调函数,在所有的钩子执行完毕时候调用。举个例子:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20let hook = new AsyncParallelHook(['name'])
hook.tapAsync('task1', function(name, cb) {
setTimeout(()=> {
console.log(`task1`)
// cb 回调函数负责通知,该异步已经执行完毕
// 如果不执行cb,则就远不会调用 callAsync 的回调
cb()
}, 1000)
})
hook.tapAsync('task2', function(name, cb) {
setTimeout(()=> {
console.log(`task2`)
// cb 回调函数负责通知,该异步已经执行完毕
// 如果不执行cb,则就远不会调用 callAsync 的回调
cb()
}, 1000)
})
hook.callAsync('ll', function() {
console.log('task end')
})
异步钩子还支持使用 promise来调用。使用 tapPromise、promise函数来注册调用钩子,例子如下:
1 | let hook = new AsyncParallelHookPromise(['name']) |
所以异步钩子也是保存了一个数组,在调用的时候依次执行回调函数。同样简单实现一下这个功能。
为了确定异步方法是否全部完成,使用了一个计数器;代码如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19class AsyncParallelHook{
constructor(args){
this.tasks = []
}
callAsync(...args){
const finalCallback = args.pop()
let index = 0
let done = () => {
index++;
if (index === this.tasks.length) {
finalCallback()
}
}
this.tasks.forEach((task) => task(...args, done))
}
tapAsync(evtName, task) {
this.tasks.push(task)
}
}
如果是promise版本的话实现就会非常简单,因为有Promise.all 方法1
2
3
4
5
6
7
8
9
10
11
12class AsyncParallelHookPromise{
constructor(args){
this.tasks = []
}
promise(...args){
let tasks = this.tasks.map((task) => task(...args))
return Promise.all(tasks)
}
tapPromise(evtName, task) {
this.tasks.push(task)
}
}
AsyncParallelBailHook
和同步钩子一样,异步钩子也有保险方法,当cb函数返回非undefined值,会跳过后面的钩子直接执行callAsync的回调。举个例子:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17let hook = new AsyncParallelBailHook(['name'])
hook.tapAsync('task1', function(name, cb) {
setTimeout(()=> {
console.log(`task1`)
}, 1000)
cb('if not undefined go task end')
})
hook.tapAsync('task2', function(name, cb) {
setTimeout(()=> {
console.log(`task2`)
cb()
}, 1000)
})
hook.callAsync('ll', function() {
console.log('task end')
})
简单实现一下异步保险勾子。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27class AsyncParallelBailHook{
constructor(args){
this.tasks = []
}
callAsync(...args){
let finalCallback = args.pop()
let index = 0
let result = true
let done = (...args) => {
index++;
if (args || args.length) {
result = false
}
if ((index === this.tasks.length || result === false) && finalCallback) {
finalCallback()
finalCallback = undefined
}
}
this.tasks.every((task) => {
task(...args, done)
return result
})
}
tapAsync(evtName, task) {
this.tasks.push(task)
}
}
AsyncSeriesHook
上面是异步并行方法,当然也有异步串行的方法。异步串行的钩子会按顺序执行钩子函数,只有上一个异步钩子函数执行完毕才会执行下一下。举个例子:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16let hook = new AsyncSeriesHook(['name'])
hook.tapAsync('task1', function(name, cb) {
setTimeout(()=> {
console.log(`task1`)
cb()
}, 1000)
})
hook.tapAsync('task2', function(name, cb) {
setTimeout(()=> {
console.log(`task2`)
cb()
}, 500)
})
hook.callAsync('ll', function() {
console.log('task end')
})
执行以上代码,会在 1秒之后先输出 task1,再过500ms之后再输出task2。它是一个串行的钩子,基本逻辑有一点不一样,我们要控制他一个一个执行。简单实现代码如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20class AsyncSeriesHook{
constructor(args){
this.tasks = []
}
callAsync(...args){
let index = 0
let finalCallback = args.pop()
let next = () => {
if (this.tasks.length === index) {
return finalCallback()
}
let task = this.tasks[index++]
task(...args, next)
}
next()
}
tapAsync(evtName, task) {
this.tasks.push(task)
}
}
AsyncSeriesBailHook
异步串行的保险钩子,当cb函数返回非undefined值,会跳过后面的钩子直接执行callAsync的回调。举个例子:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17let hook = new AsyncSeriesBailHook(['name'])
hook.tapAsync('task1', function(name, cb) {
setTimeout(()=> {
console.log(`task1`)
cb('if not null go task end')
// cb()
}, 1000)
})
hook.tapAsync('task2', function(name, cb) {
setTimeout(()=> {
console.log(`task2`)
cb()
}, 500)
})
hook.callAsync('ll', function() {
console.log('task end')
})
简单实现一下1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20class AsyncSeriesBailHook{
constructor(args){
this.tasks = []
}
callAsync(...args){
let index = 0
let finalCallback = args.pop()
let next = (data) => {
if (this.tasks.length === index || data) {
return finalCallback()
}
let task = this.tasks[index++]
task(args, next)
}
next()
}
tapAsync(evtName, task) {
this.tasks.push(task)
}
}
AsyncSeriesWaterfallHook
异步串行的瀑布流钩子的回调函数接收两个参数err、nextValue。如果err不为null瀑布流将会中止执行;如果设置了nextValue,这个值将会被传给下一个钩子回调。举个例子:
1 | let hook = new AsyncSeriesWaterfallHook(['name']) |
整理一下思路,其实就是在AsyncSeriesHook 上添加一个错误判断,再更改传参的值而已。
1 | class AsyncSeriesWaterfallHook{ |
如果使用promise,实现会更加的方便。因为Array.prototype.reduce无法处理异步方法,但是可以使用promise。实现如下:
1 | class AsyncSeriesWaterfallHook{ |