webpack本质上是一种事件流的机制,它的工作流程就是将各个插件串联起来,而实现这一切的核心就是Tapable,webpack中最核心的负责编译的Compiler和负责创建bundles的Compilation都是Tapable的实例。
Webpack 可以认为是一种基于事件流的编程范例,内部的工作流程都是基于 插件 机制串接起来; 而将这些插件粘合起来的就是webpack自己写的基础类 Tapable 是,plugin方法就是该类暴露出来的; 基于该类规范而其的 Webpack 体系保证了插件的有序性,使得整个系统非常有弹性,扩展性很好;然而有一个致命的缺点就是调试、看源码真是很痛苦,各种跳来跳去;(基于事件流的写法,和程序语言中的 goto 语句很类似)
在Tapable1.0之前,也就是webpack3及其以前使用的Tapable,提供了包括
- plugin(name:string, handler:function)注册插件到Tapable对象中
- apply(…pluginInstances: (AnyPlugin|function)[])调用插件的定义,将事件监听器注册到Tapable实例注册表中
- applyPlugins*(name:string, …)多种策略细致地控制事件的触发,包括applyPluginsAsync、applyPluginsParallel等方法实现对事件触发的控制,实现
- 多个事件连续顺序执行
- 并行执行
- 异步执行
- 一个接一个地执行插件,前面的输出是后一个插件的输入的瀑布流执行顺序
- 在允许时停止执行插件,即某个插件返回了一个undefined的值,即退出执行
我们可以看到,Tapable就像nodejs中EventEmitter,提供对事件的注册on和触发emit,理解它很重要
Tapable中的钩子函数
tapable包暴露出很多钩子类,这些类可以用来为插件创建钩子函数。
从 https://github.com/webpack/tapable,lib/index.js看出,tapable提供了九种钩子:
const { | |
SyncHook, | |
SyncBailHook, | |
SyncWaterfallHook, | |
SyncLoopHook, | |
AsyncParallelHook, | |
AsyncParallelBailHook, | |
AsyncSeriesHook, | |
AsyncSeriesBailHook, | |
AsyncSeriesWaterfallHook | |
} = require("tapable"); |
所有钩子类的构造函数都接收一个可选的参数,这个参数是一个由字符串参数组成的数组,如下:
const hook = new SyncHook(["arg1", "arg2", "arg3"]);
new Hook 新建钩子
- tapable 暴露出来的都是类方法,new 一个类方法获得我们需要的钩子。
- class 接受数组参数options,非必传。类方法会根据传参,接受同样数量的参数。
下面我们就详细介绍一下钩子的用法,以及一些钩子类实现的原理。
hooks概览
常用的钩子主要包含以下几种,分为同步和异步,异步又分为并发执行和串行执行,如下图:
首先,整体感受下钩子的用法,如下
钩子名称 | 执行方式 | 使用要点 |
SyncHook | 同步串行 | 不关心监听函数的返回值 |
SyncBailHook | 同步串行 | 只要监听函数中有一个函数的返回值不为 null,则跳过剩下所有的逻辑 |
SyncWaterfallHook | 同步串行 | 上一个监听函数的返回值可以传给下一个监听函数 |
SyncLoopHook | 同步循环 | 当监听函数被触发的时候,如果该监听函数返回true时则这个监听函数会反复执行,如果返回 undefined 则表示退出循环 |
AsyncParallelHook | 异步并发 | 不关心监听函数的返回值 |
AsyncParallelBailHook | 异步并发 | 只要监听函数的返回值不为 null,就会忽略后面的监听函数执行,直接跳跃到callAsync等触发函数绑定的回调函数,然后执行这个被绑定的回调函数 |
AsyncSeriesHook | 异步串行 | 不关系callback()的参数 |
AsyncSeriesBailHook | 异步串行 | callback()的参数不为null,就会直接执行callAsync等触发函数绑定的回调函数 |
AsyncSeriesWaterfallHook | 异步串行 | 上一个监听函数的中的callback(err, data)的第二个参数,可以作为下一个监听函数的参数 |
钩子分为同步VS 异步,细分为 并行VS串行,在根据返回值,细分为不同种类。
- BasicHook: 执行每一个,不关心函数的返回值,有 SyncHook、AsyncParallelHook、AsyncSeriesHook。
- BailHook: 顺序执行 Hook,遇到第一个结果 result !== undefined 则返回,不再继续执行。有:SyncBailHook、AsyncSeriseBailHook, AsyncParallelBailHook。
- WaterfallHook: 类似于 reduce,如果前一个 Hook 函数的结果 result !== undefined,则 result 会作为后一个 Hook 函数的第一个参数。既然是顺序执行,那么就只有 Sync 和 AsyncSeries 类中提供这个Hook:SyncWaterfallHook,AsyncSeriesWaterfallHook
- LoopHook: 不停的循环执行 Hook,直到所有函数结果 result === undefined。同样的,由于对串行性有依赖,所以只有 SyncLoopHook 和 AsyncSeriseLoopHook (PS:暂时没看到具体使用 Case)
Tabable 关键词解析
type | function |
Hook | 所有钩子的后缀 |
Waterfall | 同步方法,但是它会传值给下一个函数 |
Bail | 熔断:当函数有任何返回值,就会在当前执行函数停止 |
Loop | 监听函数返回true表示继续循环,返回undefine表示结束循环 |
Sync | 同步方法 |
AsyncSeries | 异步串行钩子 |
AsyncParallel | 异步并行执行钩子 |
我们可以根据自己的开发需求,选择适合的同步/异步钩子。
Tapable Hook类
class Hook { | |
constructor(args) { | |
if(!Array.isArray(args)) args = []; | |
this._args = args; // 实例钩子的时候的string类型的数组 | |
this.taps = []; // 消费者 | |
this.interceptors = []; // interceptors | |
this.call = this._call = // 以sync类型方式来调用钩子 | |
this._createCompileDelegate("call", "sync"); | |
this.promise = | |
this._promise = // 以promise方式 | |
this._createCompileDelegate("promise", "promise"); | |
this.callAsync = | |
this._callAsync = // 以async类型方式来调用 | |
this._createCompileDelegate("callAsync", "async"); | |
this._x = undefined; // | |
} | |
_createCall(type) { | |
return this.compile({ | |
taps: this.taps, | |
interceptors: this.interceptors, | |
args: this._args, | |
type: type | |
}); | |
} | |
_createCompileDelegate(name, type) { | |
const lazyCompileHook = (...args) => { | |
this[name] = this._createCall(type); | |
return this[name](...args); | |
}; | |
return lazyCompileHook; | |
} | |
// 调用tap 类型注册 | |
tap(options, fn) { | |
// ... | |
options = Object.assign({ type: "sync", fn: fn }, options); | |
// ... | |
this._insert(options); // 添加到 this.taps中 | |
} | |
// 注册 async类型的钩子 | |
tapAsync(options, fn) { | |
// ... | |
options = Object.assign({ type: "async", fn: fn }, options); | |
// ... | |
this._insert(options); // 添加到 this.taps中 | |
} | |
注册 promise类型钩子 | |
tapPromise(options, fn) { | |
// ... | |
options = Object.assign({ type: "promise", fn: fn }, options); | |
// ... | |
this._insert(options); // 添加到 this.taps中 | |
} | |
} |
每次都是调用tap、tapSync、tapPromise注册不同类型的插件钩子,通过调用call、callAsync 、promise方式调用。其实调用的时候为了按照一定的执行策略执行,调用compile方法快速编译出一个方法来执行这些插件。
tabpack提供了同步&异步绑定钩子的方法,并且他们都有绑定事件和执行事件对应的方法。
Async* | Sync* |
绑定:tapAsync/tapPromise/tap | 绑定:tap |
执行:callAsync/promise | 执行:call |
call/callAsync 执行绑定事件
const hook1 = new SyncHook(["arg1", "arg2", "arg3"]); | |
//绑定事件到webapck事件流 | |
hook1.tap('hook1', (arg1, arg2, arg3) => console.log(arg1, arg2, arg3)) //1,2,3 | |
//执行绑定的事件 | |
hook1.call(1,2,3) |
举个栗子
- 定义一个Car方法,在内部hooks上新建钩子。分别是同步钩子 accelerate、break(accelerate接受一个参数)、异步钩子calculateRoutes
- 使用钩子对应的绑定和执行方法
- calculateRoutes使用tapPromise可以返回一个promise对象。
//引入tapable | |
const { | |
SyncHook, | |
AsyncParallelHook | |
} = require('tapable'); | |
//创建类 | |
class Car { | |
constructor() { | |
this.hooks = { | |
accelerate: new SyncHook(["newSpeed"]), | |
break: new SyncHook(), | |
calculateRoutes: new AsyncParallelHook(["source", "target", "routesList"]) | |
}; | |
} | |
} | |
const myCar = new Car(); | |
//绑定同步钩子 | |
myCar.hooks.break.tap("WarningLampPlugin", () => console.log('WarningLampPlugin')); | |
//绑定同步钩子 并传参 | |
myCar.hooks.accelerate.tap("LoggerPlugin", newSpeed => console.log(`Accelerating to ${newSpeed}`)); | |
//绑定一个异步Promise钩子 | |
myCar.hooks.calculateRoutes.tapPromise("calculateRoutes tapPromise", (source, target, routesList, callback) => { | |
// return a promise | |
return new Promise((resolve,reject)=>{ | |
setTimeout(()=>{ | |
console.log(`tapPromise to ${source}${target}${routesList}`) | |
resolve(); | |
},1000) | |
}) | |
}); | |
//执行同步钩子 | |
myCar.hooks.break.call(); | |
myCar.hooks.accelerate.call('hello'); | |
console.time('cost'); | |
//执行异步钩子 | |
myCar.hooks.calculateRoutes.promise('i', 'love', 'tapable').then(() => { | |
console.timeEnd('cost'); | |
}, err => { | |
console.error(err); | |
console.timeEnd('cost'); | |
}) |
运行结果
WarningLampPlugin | |
Accelerating to hello | |
tapPromise to ilovetapable | |
cost: 1003.898ms |
calculateRoutes也可以使用tapAsync绑定钩子,注意:此时用callback结束异步回调。
myCar.hooks.calculateRoutes.tapAsync("calculateRoutes tapAsync", (source, target, routesList, callback) => { | |
// return a promise | |
setTimeout(() => { | |
console.log(`tapAsync to ${source}${target}${routesList}`) | |
callback(); | |
}, 2000) | |
}); | |
myCar.hooks.calculateRoutes.callAsync('i', 'like', 'tapable', err => { | |
console.timeEnd('cost'); | |
if(err) console.log(err) | |
}) |
sync* 钩子
对于Sync*类型的钩子来说。
- 注册在该钩子下面的插件的执行顺序都是顺序执行。
- 只能使用tap注册,不能使用tapPromise和tapAsync注册
// 所有的钩子都继承于Hook | |
class Sync* extends Hook { | |
tapAsync() { // Sync*类型的钩子不支持tapAsync | |
throw new Error("tapAsync is not supported on a Sync*"); | |
} | |
tapPromise() {// Sync*类型的钩子不支持tapPromise | |
throw new Error("tapPromise is not supported on a Sync*"); | |
} | |
compile(options) { // 编译代码来按照一定的策略执行Plugin | |
factory.setup(this, options); | |
return factory.create(options); | |
} | |
} |
同步串行
SyncHook
不关心监听函数的返回值
SyncHook的用法及实现
const { SyncHook } = require("tapable"); | |
let queue = new SyncHook(['name']); //所有的构造函数都接收一个可选的参数,这个参数是一个字符串的数组。 | |
// 订阅-》 注册监听函数 | |
queue.tap('1', function (name, name2) {// tap 的第一个参数是用来标识订阅的函数的 | |
console.log(name, name2, 1); | |
return '1' | |
}); | |
queue.tap('2', function (name) { | |
console.log(name, 2); | |
}); | |
queue.tap('3', function (name) { | |
console.log(name, 3); | |
}); | |
// 发布 | |
queue.call('webpack', 'webpack-cli');// 发布的时候触发订阅的函数 同时传入参数 | |
// 执行结果: | |
/* | |
webpack undefined 1 // 传入的参数需要和new实例的时候保持一致,否则获取不到多传的参数 | |
webpack 2 | |
webpack 3 | |
*/ |
通过上面如何使用的案例看出,主要是三个步骤(以同步钩子为例)
- new SyncHook(['xx']) 实例化Hook
- hook.tap('xxx', () => {}) 注册钩子
- hook.call(args) 调用钩子
原理
SyncHook是一个很典型的通过发布订阅方式实现的
class SyncHook_MY{ | |
constructor(){ | |
this.hooks = []; | |
} | |
// 订阅 | |
tap(name, fn){ | |
this.hooks.push(fn); | |
} | |
// 发布 | |
call(){ | |
this.hooks.forEach(hook => hook(...arguments)); | |
} | |
} |
SyncBailHook
只要监听函数中有一个函数的返回值不为 null,则跳过剩下所有的逻辑
SyncBailHook的用法及实现
SyncBailHook为同步串行的执行关系,只要监听函数中有一个函数的返回值不为 null,则跳过剩下所有的逻辑,用法如下:
const { | |
SyncBailHook | |
} = require("tapable"); | |
let queue = new SyncBailHook(['name']); | |
queue.tap('1', function (name) { | |
console.log(name, 1); | |
}); | |
queue.tap('2', function (name) { | |
console.log(name, 2); | |
return 'wrong' | |
}); | |
queue.tap('3', function (name) { | |
console.log(name, 3); | |
}); | |
queue.call('webpack'); | |
// 执行结果: | |
/* | |
webpack 1 | |
webpack 2 | |
*/ |
原理
// 钩子是同步的,bail -> 保险 | |
class SyncBailHook { | |
// args => ["name"] | |
constructor() { | |
this.tasks = []; | |
} | |
tap(name, task) { | |
this.tasks.push(task); | |
} | |
call(...args) { | |
// 当前函数的返回值 | |
let ret; | |
// 当前要先执行第一个 | |
let index = 0; | |
do { | |
ret = this.tasks[index++](...args); | |
} while (ret === undefined && index < this.tasks.length); | |
} | |
} |
SyncWaterfallHook的用法及实现
上一个监听函数的返回值可以传给下一个监听函数
SyncWaterfallHook为同步串行的执行关系,上一个监听函数的返回值可以传给下一个监听函数,用法如下:
const { | |
SyncBailHook | |
} = require("tapable"); | |
let queue = new SyncBailHook(['name']); | |
queue.tap('1', function (name) { | |
console.log(name, 1); | |
}); | |
queue.tap('2', function (name) { | |
console.log(name, 2); | |
return 'wrong' | |
}); | |
queue.tap('3', function (name) { | |
console.log(name, 3); | |
}); | |
queue.call('webpack'); | |
// 执行结果: | |
/* | |
webpack 1 | |
webpack 2 | |
*/ |
SyncWaterfallHook的实现:
// 钩子是同步的 | |
class SyncWaterfallHook { | |
// args => ["name"] | |
constructor() { | |
this.tasks = []; | |
} | |
tap(name, task) { | |
this.tasks.push(task); | |
} | |
call(...args) { | |
let [first, ...others] = this.tasks; | |
let ret = first(...args); | |
others.reduce((a, b) => { | |
return b(a); | |
}, ret); | |
} | |
} | |
// 简化版 | |
class SyncBailHook_MY { | |
constructor() { | |
this.hooks = []; | |
} | |
// 订阅 | |
tap(name, fn) { | |
this.hooks.push(fn); | |
} | |
// 发布 | |
call() { | |
for (let i = 0, l = this.hooks.length; i < l; i++) { | |
let hook = this.hooks[i]; | |
let result = hook(...arguments); | |
if (result) { | |
break; | |
} | |
} | |
} | |
} |
SyncLoopHook的用法及实现
当监听函数被触发的时候,如果该监听函数返回true时则这个监听函数会反复执行,如果返回 undefined 则表示退出循环
SyncLoopHook为同步循环的执行关系,当监听函数被触发的时候,如果该监听函数返回true时则这个监听函数会反复执行,如果返回 undefined 则表示退出循环,用法如下:
const { | |
SyncWaterfallHook | |
} = require("tapable"); | |
let queue = new SyncWaterfallHook(['name']); | |
// 上一个函数的返回值可以传给下一个函数 | |
queue.tap('1', function (name) { | |
console.log(name, 1); | |
return 1; | |
}); | |
queue.tap('2', function (data) { | |
console.log(data, 2); | |
return 2; | |
}); | |
queue.tap('3', function (data) { | |
console.log(data, 3); | |
}); | |
queue.call('webpack'); | |
// 执行结果: | |
/* | |
webpack 1 | |
1 2 | |
2 3 | |
*/ |
SyncLoopHook的实现:
// 钩子是同步的 | |
class SyncLoopHook { | |
// args => ["name"] | |
constructor() { | |
this.tasks = []; | |
} | |
tap(name, task) { | |
this.tasks.push(task); | |
} | |
call(...args) { | |
this.tasks.forEach(task => { | |
let ret; | |
do { | |
ret = task(...args); | |
} while (ret != undefined); | |
}); | |
} | |
} |
async* 钩子
对于Async*类型钩子
- 支持tap、tapPromise、tapAsync注册
有三种注册/发布的模式,如下:
异步订阅 | 调用方法 |
tap | callAsync |
tapAsync | callAsync |
tapPromise | promise |
异步并行
AsyncParallelHook的用法及实现
不关心监听函数的返回值。
const { | |
AsyncParallelHook | |
} = require("tapable"); | |
let queue1 = new AsyncParallelHook(['name']); | |
console.time('cost'); | |
queue1.tap('1', function (name) { | |
console.log(name, 1); | |
}); | |
queue1.tap('2', function (name) { | |
console.log(name, 2); | |
}); | |
queue1.tap('3', function (name) { | |
console.log(name, 3); | |
}); | |
queue1.callAsync('webpack', err => { | |
console.timeEnd('cost'); | |
}); | |
// 执行结果 | |
/* | |
webpack 1 | |
webpack 2 | |
webpack 3 | |
cost: 4.520ms | |
*/ |
usage - tapAsync
let queue2 = new AsyncParallelHook(['name']); | |
console.time('cost1'); | |
queue2.tapAsync('1', function (name, cb) { | |
setTimeout(() => { | |
console.log(name, 1); | |
cb(); | |
}, 1000); | |
}); | |
queue2.tapAsync('2', function (name, cb) { | |
setTimeout(() => { | |
console.log(name, 2); | |
cb(); | |
}, 2000); | |
}); | |
queue2.tapAsync('3', function (name, cb) { | |
setTimeout(() => { | |
console.log(name, 3); | |
cb(); | |
}, 3000); | |
}); | |
queue2.callAsync('webpack', () => { | |
console.log('over'); | |
console.timeEnd('cost1'); | |
}); | |
// 执行结果 | |
/* | |
webpack 1 | |
webpack 2 | |
webpack 3 | |
over | |
time: 3004.411ms | |
*/ |
usage - promise
let queue3 = new AsyncParallelHook(['name']); | |
console.time('cost3'); | |
queue3.tapPromise('1', function (name, cb) { | |
return new Promise(function (resolve, reject) { | |
setTimeout(() => { | |
console.log(name, 1); | |
resolve(); | |
}, 1000); | |
}); | |
}); | |
queue3.tapPromise('1', function (name, cb) { | |
return new Promise(function (resolve, reject) { | |
setTimeout(() => { | |
console.log(name, 2); | |
resolve(); | |
}, 2000); | |
}); | |
}); | |
queue3.tapPromise('1', function (name, cb) { | |
return new Promise(function (resolve, reject) { | |
setTimeout(() => { | |
console.log(name, 3); | |
resolve(); | |
}, 3000); | |
}); | |
}); | |
queue3.promise('webpack') | |
.then(() => { | |
console.log('over'); | |
console.timeEnd('cost3'); | |
}, () => { | |
console.log('error'); | |
console.timeEnd('cost3'); | |
}); | |
/* | |
webpack 1 | |
webpack 2 | |
webpack 3 | |
over | |
cost3: 3007.925ms | |
*/ |
AsyncParallelHook的实现:
class SyncParralleHook { constructor() { this.tasks = []; | |
} tapAsync(name, task) { this.tasks.push(task); | |
} callAsync(...args) { // 拿出最终的函数 | |
let finalCallBack = args.pop(); let index = 0; // 类似Promise.all | |
let done = () => { | |
index++; if (index === this.tasks.length) { | |
finalCallBack(); | |
} | |
}; this.tasks.forEach(task => { | |
task(...args, done); | |
}); | |
} | |
} |
AsyncParallelBailHook
只要监听函数的返回值不为 null,就会忽略后面的监听函数执行,直接跳跃到callAsync等触发函数绑定的回调函数,然后执行这个被绑定的回调函数。
usage - tap
let queue1 = new AsyncParallelBailHook(['name']);console.time('cost'); | |
queue1.tap('1', function (name) { console.log(name, 1); | |
}); | |
queue1.tap('2', function (name) { console.log(name, 2); return 'wrong'}); | |
queue1.tap('3', function (name) { console.log(name, 3); | |
}); | |
queue1.callAsync('webpack', err => { console.timeEnd('cost'); | |
});// 执行结果:/* | |
webpack 1 | |
webpack 2 | |
cost: 4.975ms | |
*/ |
usage - tapAsync
let queue2 = new AsyncParallelBailHook(['name']); | |
console.time('cost1'); | |
queue2.tapAsync('1', function (name, cb) { | |
setTimeout(() => { | |
console.log(name, 1); | |
cb(); | |
}, 1000); | |
}); | |
queue2.tapAsync('2', function (name, cb) { | |
setTimeout(() => { | |
console.log(name, 2); | |
return 'wrong';// 最后的回调就不会调用了 | |
cb(); | |
}, 2000); | |
}); | |
queue2.tapAsync('3', function (name, cb) { | |
setTimeout(() => { | |
console.log(name, 3); | |
cb(); | |
}, 3000); | |
}); | |
queue2.callAsync('webpack', () => { | |
console.log('over'); | |
console.timeEnd('cost1'); | |
}); | |
// 执行结果: | |
/* | |
webpack 1 | |
webpack 2 | |
webpack 3 | |
*/ |
usage - promise
let queue3 = new AsyncParallelBailHook(['name']); | |
console.time('cost3'); | |
queue3.tapPromise('1', function (name, cb) { | |
return new Promise(function (resolve, reject) { | |
setTimeout(() => { | |
console.log(name, 1); | |
resolve(); | |
}, 1000); | |
}); | |
}); | |
queue3.tapPromise('2', function (name, cb) { | |
return new Promise(function (resolve, reject) { | |
setTimeout(() => { | |
console.log(name, 2); | |
reject('wrong');// reject()的参数是一个不为null的参数时,最后的回调就不会再调用了 | |
}, 2000); | |
}); | |
}); | |
queue3.tapPromise('3', function (name, cb) { | |
return new Promise(function (resolve, reject) { | |
setTimeout(() => { | |
console.log(name, 3); | |
resolve(); | |
}, 3000); | |
}); | |
}); | |
queue3.promise('webpack') | |
.then(() => { | |
console.log('over'); | |
console.timeEnd('cost3'); | |
}, () => { | |
console.log('error'); | |
console.timeEnd('cost3'); | |
}); | |
// 执行结果: | |
/* | |
webpack 1 | |
webpack 2 | |
error | |
cost3: 2009.970ms | |
webpack 3 | |
*/ |
AsyncSeriesHook的实现:
class SyncSeriesHook { | |
constructor() { | |
this.tasks = []; | |
} | |
tapAsync(name, task) { | |
this.tasks.push(task); | |
} | |
callAsync(...args) { | |
let finalCallback = args.pop(); | |
let index = 0; | |
let next = () => { | |
if (this.tasks.length === index) return finalCallback(); | |
let task = this.tasks[index++]; | |
task(...args, next); | |
}; | |
next(); | |
} | |
} |
异步串行
AsyncSeriesWaterfallHook的用法及实现
不关系callback()的参数
AsyncSeriesWaterfallHook为异步串行的执行关系,上一个监听函数的中的callback(err, data)的第二个参数,可以作为下一个监听函数的参数
usage - tap
const { | |
AsyncSeriesHook | |
} = require("tapable"); | |
// tap | |
let queue1 = new AsyncSeriesHook(['name']); | |
console.time('cost1'); | |
queue1.tap('1', function (name) { | |
console.log(1); | |
return "Wrong"; | |
}); | |
queue1.tap('2', function (name) { | |
console.log(2); | |
}); | |
queue1.tap('3', function (name) { | |
console.log(3); | |
}); | |
queue1.callAsync('zfpx', err => { | |
console.log(err); | |
console.timeEnd('cost1'); | |
}); | |
// 执行结果 | |
/* | |
1 | |
2 | |
3 | |
undefined | |
cost1: 3.933ms | |
*/ |
usage - tapAsync
let queue2 = new AsyncSeriesHook(['name']); | |
console.time('cost2'); | |
queue2.tapAsync('1', function (name, cb) { | |
setTimeout(() => { | |
console.log(name, 1); | |
cb(); | |
}, 1000); | |
}); | |
queue2.tapAsync('2', function (name, cb) { | |
setTimeout(() => { | |
console.log(name, 2); | |
cb(); | |
}, 2000); | |
}); | |
queue2.tapAsync('3', function (name, cb) { | |
setTimeout(() => { | |
console.log(name, 3); | |
cb(); | |
}, 3000); | |
}); | |
queue2.callAsync('webpack', (err) => { | |
console.log(err); | |
console.log('over'); | |
console.timeEnd('cost2'); | |
}); | |
// 执行结果 | |
/* | |
webpack 1 | |
webpack 2 | |
webpack 3 | |
undefined | |
over | |
cost2: 6019.621ms | |
*/ |
usage - promise
let queue3 = new AsyncSeriesHook(['name']); | |
console.time('cost3'); | |
queue3.tapPromise('1',function(name){ | |
return new Promise(function(resolve){ | |
setTimeout(function(){ | |
console.log(name, 1); | |
resolve(); | |
},1000) | |
}); | |
}); | |
queue3.tapPromise('2',function(name,callback){ | |
return new Promise(function(resolve){ | |
setTimeout(function(){ | |
console.log(name, 2); | |
resolve(); | |
},2000) | |
}); | |
}); | |
queue3.tapPromise('3',function(name,callback){ | |
return new Promise(function(resolve){ | |
setTimeout(function(){ | |
console.log(name, 3); | |
resolve(); | |
},3000) | |
}); | |
}); | |
queue3.promise('webapck').then(err=>{ | |
console.log(err); | |
console.timeEnd('cost3'); | |
}); | |
// 执行结果 | |
/* | |
webapck 1 | |
webapck 2 | |
webapck 3 | |
undefined | |
cost3: 6021.817ms | |
*/ |
原理
class AsyncSeriesHook_MY { | |
constructor() { | |
this.hooks = []; | |
} | |
tapAsync(name, fn) { | |
this.hooks.push(fn); | |
} | |
callAsync() { | |
var slef = this; | |
var args = Array.from(arguments); | |
let done = args.pop(); | |
let idx = 0; | |
function next(err) { | |
// 如果next的参数有值,就直接跳跃到 执行callAsync的回调函数 | |
if (err) return done(err); | |
let fn = slef.hooks[idx++]; | |
fn ? fn(...args, next) : done(); | |
} | |
next(); | |
} | |
} |
AsyncSeriesBailHook
callback()的参数不为null,就会直接执行callAsync等触发函数绑定的回调函数
usage - tap
const { | |
AsyncSeriesBailHook | |
} = require("tapable"); | |
// tap | |
let queue1 = new AsyncSeriesBailHook(['name']); | |
console.time('cost1'); | |
queue1.tap('1', function (name) { | |
console.log(1); | |
return "Wrong"; | |
}); | |
queue1.tap('2', function (name) { | |
console.log(2); | |
}); | |
queue1.tap('3', function (name) { | |
console.log(3); | |
}); | |
queue1.callAsync('webpack', err => { | |
console.log(err); | |
console.timeEnd('cost1'); | |
}); | |
// 执行结果: | |
/* | |
1 | |
null | |
cost1: 3.979ms | |
*/ |
usage - tapAsync
let queue2 = new AsyncSeriesBailHook(['name']); | |
console.time('cost2'); | |
queue2.tapAsync('1', function (name, callback) { | |
setTimeout(function () { | |
console.log(name, 1); | |
callback(); | |
}, 1000) | |
}); | |
queue2.tapAsync('2', function (name, callback) { | |
setTimeout(function () { | |
console.log(name, 2); | |
callback('wrong'); | |
}, 2000) | |
}); | |
queue2.tapAsync('3', function (name, callback) { | |
setTimeout(function () { | |
console.log(name, 3); | |
callback(); | |
}, 3000) | |
}); | |
queue2.callAsync('webpack', err => { | |
console.log(err); | |
console.log('over'); | |
console.timeEnd('cost2'); | |
}); | |
// 执行结果 | |
/* | |
webpack 1 | |
webpack 2 | |
wrong | |
over | |
cost2: 3014.616ms | |
*/ |
usage - promise
let queue3 = new AsyncSeriesBailHook(['name']); | |
console.time('cost3'); | |
queue3.tapPromise('1', function (name) { | |
return new Promise(function (resolve, reject) { | |
setTimeout(function () { | |
console.log(name, 1); | |
resolve(); | |
}, 1000) | |
}); | |
}); | |
queue3.tapPromise('2', function (name, callback) { | |
return new Promise(function (resolve, reject) { | |
setTimeout(function () { | |
console.log(name, 2); | |
reject(); | |
}, 2000) | |
}); | |
}); | |
queue3.tapPromise('3', function (name, callback) { | |
return new Promise(function (resolve) { | |
setTimeout(function () { | |
console.log(name, 3); | |
resolve(); | |
}, 3000) | |
}); | |
}); | |
queue3.promise('webpack').then(err => { | |
console.log(err); | |
console.log('over'); | |
console.timeEnd('cost3'); | |
}, err => { | |
console.log(err); | |
console.log('error'); | |
console.timeEnd('cost3'); | |
}); | |
// 执行结果: | |
/* | |
webpack 1 | |
webpack 2 | |
undefined | |
error | |
cost3: 3017.608ms | |
*/ |
AsyncSeriesWaterfallHook
上一个监听函数的中的callback(err, data)的第二个参数,可以作为下一个监听函数的参数
usage - tap
const { | |
AsyncSeriesWaterfallHook | |
} = require("tapable"); | |
// tap | |
let queue1 = new AsyncSeriesWaterfallHook(['name']); | |
console.time('cost1'); | |
queue1.tap('1', function (name) { | |
console.log(name, 1); | |
return 'lily' | |
}); | |
queue1.tap('2', function (data) { | |
console.log(2, data); | |
return 'Tom'; | |
}); | |
queue1.tap('3', function (data) { | |
console.log(3, data); | |
}); | |
queue1.callAsync('webpack', err => { | |
console.log(err); | |
console.log('over'); | |
console.timeEnd('cost1'); | |
}); | |
// 执行结果: | |
/* | |
webpack 1 | |
2 'lily' | |
3 'Tom' | |
null | |
over | |
cost1: 5.525ms | |
*/ |
usage - tapAsync
let queue2 = new AsyncSeriesWaterfallHook(['name']); | |
console.time('cost2'); | |
queue2.tapAsync('1', function (name, callback) { | |
setTimeout(function () { | |
console.log('1: ', name); | |
callback(null, 2); | |
}, 1000) | |
}); | |
queue2.tapAsync('2', function (data, callback) { | |
setTimeout(function () { | |
console.log('2: ', data); | |
callback(null, 3); | |
}, 2000) | |
}); | |
queue2.tapAsync('3', function (data, callback) { | |
setTimeout(function () { | |
console.log('3: ', data); | |
callback(null, 3); | |
}, 3000) | |
}); | |
queue2.callAsync('webpack', err => { | |
console.log(err); | |
console.log('over'); | |
console.timeEnd('cost2'); | |
}); | |
// 执行结果: | |
/* | |
1: webpack | |
2: 2 | |
3: 3 | |
null | |
over | |
cost2: 6016.889ms | |
*/ |
usage - promise
let queue3 = new AsyncSeriesWaterfallHook(['name']); | |
console.time('cost3'); | |
queue3.tapPromise('1', function (name) { | |
return new Promise(function (resolve, reject) { | |
setTimeout(function () { | |
console.log('1:', name); | |
resolve('1'); | |
}, 1000) | |
}); | |
}); | |
queue3.tapPromise('2', function (data, callback) { | |
return new Promise(function (resolve) { | |
setTimeout(function () { | |
console.log('2:', data); | |
resolve('2'); | |
}, 2000) | |
}); | |
}); | |
queue3.tapPromise('3', function (data, callback) { | |
return new Promise(function (resolve) { | |
setTimeout(function () { | |
console.log('3:', data); | |
resolve('over'); | |
}, 3000) | |
}); | |
}); | |
queue3.promise('webpack').then(err => { | |
console.log(err); | |
console.timeEnd('cost3'); | |
}, err => { | |
console.log(err); | |
console.timeEnd('cost3'); | |
}); | |
// 执行结果: | |
/* | |
1: webpack | |
2: 1 | |
3: 2 | |
over | |
cost3: 6016.703ms | |
*/ |
原理
class AsyncSeriesWaterfallHook_MY { | |
constructor() { | |
this.hooks = []; | |
} | |
tapAsync(name, fn) { | |
this.hooks.push(fn); | |
} | |
callAsync() { | |
let self = this; | |
var args = Array.from(arguments); | |
let done = args.pop(); | |
console.log(args); | |
let idx = 0; | |
let result = null; | |
function next(err, data) { | |
if (idx >= self.hooks.length) return done(); | |
if (err) { | |
return done(err); | |
} | |
let fn = self.hooks[idx++]; | |
if (idx == 1) { | |
fn(...args, next); | |
} else { | |
fn(data, next); | |
} | |
} | |
next(); | |
} | |
} | |
系列文章:
webpack原理(1):Webpack热更新实现原理代码分析
webpack原理(2):ES6 module在Webpack中如何Tree-shaking构建
webpack原理(3):Tapable源码分析及钩子函数作用分析
参考文章:
webpack插件机制之Tapable https://juejin.cn/post/6844903774645911566
干货!撸一个webpack插件(内含tapable详解+webpack流程) https://juejin.cn/post/6844903713312604173
webpack详解 https://juejin.cn/post/6844903573675835400
webpack4.0源码分析之Tapable https://juejin.cn/post/6844903588112629767
Webpack 源码(一)—— Tapable 和 事件流 https://segmentfault.com/a/1190000008060440