学习webpack
源码时,总是绕不开tapable
,越看越觉得它晦涩难懂,但只要理解了它的功能,学习就会容易很多。
简单来说,有一系列的同步、异步任务,我希望它们可以以多种流程执行,比如:
- 一个执行完再执行下一个,即串行执行;
- 一块执行,即并行执行;
- 串行执行过程中,可以中断执行,即有熔断机制
- 等等
而tapable
库,就帮我们实现了多种任务的执行流程,它们可以根据以下特点分类:
-
同步sync、异步async**:
task
是否包含异步代码 -
串行series、并发parallel**:前后
task
是否有执行顺序 - 是否使用promise
- 熔断bail**:是否有熔断机制
-
waterfall:前后
task
是否有数据依赖
举个例子,如果我们想要多个同步的任务 串行执行,只需要三个步骤:初始化hook、添加任务、触发任务执行:
// 引入 同步 的hook
const { SyncBailHook } = require("tapable");
// 初始化
const tasks = new SyncBailHook(['tasks'])
// 绑定一个任务
tasks.tap('task1', () => {
console.log('task1', name);
})
// 再绑定一个任务
tasks.tap('task2', () => {
console.log('task2', name);
})
// 调用call,我们的两个任务就会串行执行了,
tasks.call('done')
是不是很简单,下面我们学习下tapable
实现了哪些任务执行流程,并且是如何实现的:
一、同步事件流
如上例子所示,每一种hook
都会有两个方法,用于添加任务和触发任务执行。在同步的hook
中,分别对应tap
和call
方法。
1. 并行
所有任务一起执行
class SyncHook {
constructor() {
// 用于保存添加的任务
this.tasks = []
}
tap(name, task) {
// 注册事件
this.tasks.push(task)
}
call(...args) {
// 把注册的事件依次调用,无特殊处理
this.tasks.forEach(task => task(...args))
}
}
2. 串行可熔断
如果其中一个
task
有返回值(不为undefined
),就会中断tasks的调用
class SyncBailHook {
constructor() {
// 用于保存添加的任务
this.tasks = []
}
tap(name, task) {
this.tasks.push(task)
}
call(...args) {
for (let i = 0; i < this.tasks.length; i++) {
const result = this.tasks[i](...args)
// 有返回值的话,就会中断调用
if (result !== undefined) {
break
}
}
}
}
3. 串行瀑布流
task
的计算结果会作为下一个task
的参数,以此类推
class SyncWaterfallHook {
constructor() {
this.tasks = []
}
tap(name, task) {
this.tasks.push(task)
}
call(...args) {
const [first, ...others] = this.tasks
const result = first(...args)
// 上一个task的返回值会作为下一个task的函数参数
others.reduce((result, task) => {
return task(result)
}, result)
}
}
4. 串行可循环
如果
task
有返回值(返回值不为undefined
),就会循环执行当前task
,直到返回值为undefined
才会执行下一个task
class SyncLoopHook {
constructor() {
this.tasks = []
}
tap(name, task) {
this.tasks.push(task)
}
call(...args) {
// 当前执行task的index
let currentTaskIdx = 0
while (currentTaskIdx < this.tasks.length) {
let task = this.tasks[currentTaskIdx]
const result = task(...args)
// 只有返回为undefined的时候才会执行下一个task,否则一直执行当前task
if (result === undefined) {
currentTaskIdx++
}
}
}
}
二、异步事件流
异步事件流中,绑定和触发的方法都会有两种实现:
- 使用
promise
:tapPromise
绑定、promise
触发 - 非
promise
:tapAsync
绑定、callAsync
触发
注意事项:
既然我们要控制异步tasks
的执行流程,那我们必须要知道它们执行完的时机:
-
使用
promise
的hook
,任务中resolve
的调用就代表异步执行完毕了;// 使用promise方法的例子 // 初始化异步并行的hook const asyncHook = new AsyncParallelHook('async') // 添加task // tapPromise需要返回一个promise asyncHook.tapPromise('render1', (name) => { return new Promise((resolve, reject) => { setTimeout(() => { console.log('render1', name); resolve() }, 1000); }) }) // 再添加一个task // tapPromise需要返回一个promise asyncHook.tapPromise('render2', (name) => { return new Promise((resolve, reject) => { setTimeout(() => { console.log('render2', name); resolve() }, 1000); }) }) // 传入的两个异步任务就可以串行执行了,并在执行完毕后打印done asyncHook.promise().then( () => { console.log('done'); })
-
但在使用非
promise
的hook
时,异步任务执行完毕的时机我们就无从获取了。所以我们规定传入的task
的最后一个参数参数为一个函数,并且在异步任务执行完毕后执行它,这样我们能获取执行完毕的时机,如下例所示:const asyncHook = new AsyncParallelHook('async') // 添加task asyncHook.tapAsync('example', (data, cb) => { setTimeout(() => { console.log('example', name); // 在异步操作完成时,调用回调函数,表示异步任务完成 cb() }, 1000); }) // 添加task asyncHook.tapAsync('example1', (data, cb) => { setTimeout(() => { console.log('example1', name); // 在异步操作完成时,调用回调函数,表示异步任务完成 cb() }, 1000); }) // 传入的两个异步任务就可以串行执行了,并在执行完毕后打印done asyncHook.callAsync('done', () => { console.log('done') })
1. 并行执行
task
一起执行,所有异步事件执行完成后,执行最后的回调。类似promise.all
NOTE: callAsync
中计数器的使用,类似于promise.all
的实现原理
class AsyncParallelHook {
constructor() {
this.tasks = []
}
tapAsync(name, task) {
this.tasks.push(task)
}
callAsync(...args) {
// 最后一个参数为,流程结束的回调
const finalCB = args.pop()
let index = 0
// 这就是每个task执行完成时调用的回调函数
const CB = () => {
++index
// 当这个回调函数调用的次数等于tasks的个数时,说明任务都执行完了
if (index === this.tasks.length) {
// 调用流程结束的回调函数
finalCB()
}
}
this.tasks.forEach(task => task(...args, CB))
}
// task是一个promise生成器
tapPromise(name, task) {
this.tasks.push(task)
}
// 使用promise.all实现
promise(...args) {
const tasks = this.tasks.map(task => task(...args))
return Promise.all(tasks)
}
}
2. 异步串行执行
所有
tasks
串行执行,一个tasks
执行完了在执行下一个
NOTE:callAsync
的实现与使用,类似于generate
执行器co
和async await
的原理
NOTE:promise
的实现与使用,就是面试中常见的 异步任务调度题 的正解。比如,实现每隔一秒打印1次,打印5次。
class AsyncSeriesHook {
constructor() {
this.tasks = []
}
tapAsync(name, task) {
this.tasks.push(task)
}
callAsync(...args) {
const finalCB = args.pop()
let index = 0
// 这就是每个task异步执行完毕之后调用的回调函数
const next = () => {
let task = this.tasks[index++]
if (task) {
// task执行完毕之后,会调用next,继续执行下一个task,形成递归,直到任务全部执行完
task(...args, next)
} else {
// 任务完毕之后,调用流程结束的回调函数
finalCB()
}
}
next()
}
tapPromise(name, task) {
this.tasks.push(task)
}
promise(...args) {
let [first, ...others] = this.tasks
return others.reduce((p, n) =>{
// then函数中返回另一个promise,可以实现promise的串行执行
return p.then(() => n(...args))
},first(...args))
}
}
3. 串行瀑布流
异步
task
串行执行,task
的计算结果会作为下一个task
的参数,以此类推。task
执行结果通过cb
回调函数向下传递。
class AsyncWaterfallHook {
constructor() {
this.tasks = []
}
tapAsync(name, task) {
this.tasks.push(task)
}
callAsync(...args) {
const [first] = this.tasks
const finalCB = args.pop()
let index = 1
// 这就是每个task异步执行完毕之后调用的回调函数,其中ret为上一个task的执行结果
const next = (error, ret) => {
if(error !== undefined) {
return
}
let task = this.tasks[index++]
if (task) {
// task执行完毕之后,会调用next,继续执行下一个task,形成递归,直到任务全部执行完
task(ret, next)
} else {
// 任务完毕之后,调用流程结束的回调函数
finalCB(ret)
}
}
first(...args, next)
}
tapPromise(name, task) {
this.tasks.push(task)
}
promise(...args) {
let [first, ...others] = this.tasks
return others.reduce((p, n) =>{
// then函数中返回另一个promise,可以实现promise的串行执行
return p.then(() => n(...args))
}, first(...args))
}
}
总结
学了tapable
的一些hook
,你能扩展到很多东西:
promise.all
-
co
模块 async await
- 面试中的经典手写代码题:任务调度系列
- 设计模式之监听者模式
- 设计模式之发布订阅者模式
你都可以去实现,用于巩固和拓展相关知识。
我们在学习tapable
时,重点不在于这个库的细节和使用,而在于多个任务有可能的执行流程以及流程的实现原理,它们是众多实际问题的抽象模型,掌握了它们,你就可以在实际开发中和面试中举一反三,举重若轻。