webpack4.0之tapable

tapable

tapable是一个钩子注册、调用库。tapable包暴露了许多Hook类,可用于为插件创建钩子。基本使用如下:

1
2
3
4
5
6
7
8
9
let { SyncHook } = require('tapable')

let hook = new SyncHook(['name'])
// 注册一个钩子
hook.tap('test', function(name) {
console.log('the test hook', name)
})
//在合适的时机调用
hook.call('call hook test')

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
25
let { 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 hookthe 2th hook。它的机制很简单,就是将所有的钩子函数保存在了一个数组里,在钩子被调用的时候依次执行。以下是简单实现代码:

1
2
3
4
5
6
7
8
9
10
11
class 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
9
let 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
16
class 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
9
let 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class SyncWaterfallHook{
constructor(args){
this.tasks = []
}
call(...args){
const [first, ...others] = this.tasks

let ret = first(...args)
others.reduce((preValue, next) => {
return next(preValue)
}, ret)

}
tap(evtName, task) {
this.tasks.push(task)
}
}
AsyncParallelHook

异步并发钩子,注册的异步钩子将会以并发模式运行。异步钩子居别于同步钩子,需要使用.tapAsync.callAsync来注册和调用钩子函数,并且.callAsync方法还提供了一个回调函数,在所有的钩子执行完毕时候调用。举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
let 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来调用。使用 tapPromisepromise函数来注册调用钩子,例子如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
let hook = new AsyncParallelHookPromise(['name'])
hook.tapPromise('task1', function(name, cb) {
return new Promise(function(resolve, reject) {
setTimeout(()=> {
console.log(`task1`)

// reject()
resolve()
}, 1000)
})
})
hook.tapPromise('task2', function(name, cb) {
return new Promise(function(resolve, reject) {
setTimeout(()=> {
console.log(`task2`)

// reject()
resolve()
}, 1000)
})
})
hook.promise('ll').then(function() {
console.log('task end')
})

所以异步钩子也是保存了一个数组,在调用的时候依次执行回调函数。同样简单实现一下这个功能。
为了确定异步方法是否全部完成,使用了一个计数器;代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class 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
12
class 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
17
let 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
27
class 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
16
let 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
20
class 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
17
let 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
20
class 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

异步串行的瀑布流钩子的回调函数接收两个参数errnextValue。如果err不为null瀑布流将会中止执行;如果设置了nextValue,这个值将会被传给下一个钩子回调。举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
let hook = new AsyncSeriesWaterfallHook(['name'])
hook.tapAsync('task1', function(name, cb) {
setTimeout(()=> {
console.log(`task1 ${name}`)
// cb(err, nextValue)
cb(null, '传给下一个人')
}, 1000)
})
hook.tapAsync('task2', function(name, cb) {
setTimeout(()=> {
console.log(`task2 ${name}`)
cb()
}, 1000)
})
hook.callAsync('ll', function() {
console.log('task end')
})

整理一下思路,其实就是在AsyncSeriesHook 上添加一个错误判断,再更改传参的值而已。

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
class AsyncSeriesWaterfallHook{
constructor(args){
this.tasks = []
}
callAsync(...args){
let index = 0
let finalCallback = args.pop()
let next = (err, data) => {
let task = this.tasks[index]
if (!task || err) {
return finalCallback()
}
if(index === 0) {
task(...args, next)
} else {
task(data || args, next)
}
index ++

}
next()
}
tapAsync(evtName, task) {
this.tasks.push(task)
}
}

如果使用promise,实现会更加的方便。因为Array.prototype.reduce无法处理异步方法,但是可以使用promise。实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class AsyncSeriesWaterfallHook{
constructor(args){
this.tasks = []
}
promise(...args){
const [first, ...others] = this.tasks

others.reduce((pre, next) => {
return pre.then((data) => next(data || args))
}, first(...args))
}
tapPromise(evtName, task) {
this.tasks.push(task)
}
}