Tapable是何物
Tapable 承包了 webpack 最重要的事件工作机制,包括 webapck 源码中高频的两大对象(compiler , compilation)都是继承自 Tabable 类的对象。这些对象都有 Tapable 的注册调用插件的功能,并向外暴露出各自的执行顺序以及 hook 类型。
Tabable 的钩子(hook)
const {
SyncHook,
SyncBailHook,
SyncWaterfallHook,
SyncLoopHook,
AsyncParallelHook,
AsyncParallelBailHook,
AsyncSeriesHook,
AsyncSeriesBailHook,
AsyncSeriesWaterfallHook
} = require("tapable");
钩子类型如下:(返回值的定义请看 应用篇的触发事件的描述内容,可知返回值的来源有三种)
- 基础类型。
hook名称中不包含Bail,Waterfall,Loop的,都属于基本类型。也就是SyncHook,AsyncParallelHook,AsyncSeriesHook(同步,异步并行,异步串行)。这类钩子,只会简单地调用注册的事件回调, 不关心监听函数的返回值(即,即是有返回值,也不会传递到下一个回调事件中去)。 Bail类型。SyncBailHook,AsyncParallelBailHook,AsyncSeriesBailHook。当一个事件回调运行时返回的值是undefined时或无返回值的时候,才会继续执行后面的回调事件。否则,则停止。Waterfall类型。SyncWaterfallHook,AsyncSeriesWaterfallHook(注意,没有AsyncParallelWaterfallHook并行瀑布类型)。如果一个事件回调返回值不是undefined的话,就会将该值传递给下一个回调事件。Loop类型。SyncLoopHook。当注册的回调事件执行后返回值不是undefined的话,将会重新注册一个回调事件去执行,直到返回值是undefined(或无返回值)为止。
Each hook can be tapped with one or several functions. How they are executed depends on the hook type:
- Basic hook (without “Waterfall”, “Bail” or “Loop” in its name). This hook simply calls every function it tapped in a row.
- Waterfall. A waterfall hook also calls each tapped function in a row. Unlike the basic hook, it passes a return value from each function to the next function.
- Bail. A bail hook allows exiting early. When any of the tapped function returns anything, the bail hook will stop executing the remaining ones.
- Loop. When a plugin in a loop hook returns a non-undefined value the hook will restart from the first plugin. It will loop until all plugins return undefined.
类型 描述 Basic 基础类型,单纯的调用注册的事件回调,并不关心其内部的运行逻辑。 Bail 保险类型,当一个事件回调在运行时返回的值不为 undefined时,停止后面事件回调的执行。Waterfall 瀑布类型,如果当前执行的事件回调返回值不为 undefined,那么就把下一个事件回调的第一个参数替换成这个值。Loop 循环类型,如果当前执行的事件回调的返回值不是 undefined,重新从第一个注册的事件回调处执行,直到当前执行的事件回调没有返回值。下文有详细解释。
总而言之,以上四种类型的钩子,大概应用的场景应是:
- 不管有没有返回值,对预期结果都不会产生任何影响的,使用基础类型即可
- 预期是没有返回值的,如果有返回值,则打断执行,尽早地反馈结果的,可以使用Bail类型
- 如果有一份传家宝,需要代代相传的,则适合waterfall类型啦
- 偏执型事件专用,如果一定要结果是没有返回值的,如果有,则生生世世轮回不止的,就是Loop类型了
钩子的回调事件注册方式
- 同步。以
Sync开头的钩子,只能用同步的方式去注册回调事件,如myHook.tap() - 异步串行。可以用
myHook.tap(),myHook.tapAsync(),myHook.toPromise()注册事件,事件会串行地注册 - 异步并行。可以用
myHook.tap(),myHook.tapAsync(),myHook.toPromise()注册事件,但事件执行跟异步串行不一样,它会并行地注册回调事件。
Additionally, hooks can be synchronous or asynchronous. To reflect this, there’re “Sync”, “AsyncSeries”, and “AsyncParallel” hook classes:
- Sync. A sync hook can only be tapped with synchronous functions (using
myHook.tap()).- AsyncSeries. An async-series hook can be tapped with synchronous, callback-based and promise-based functions (using
myHook.tap(),myHook.tapAsync()andmyHook.tapPromise()). They call each async method in a row.- AsyncParallel. An async-parallel hook can also be tapped with synchronous, callback-based and promise-based functions (using
myHook.tap(),myHook.tapAsync()andmyHook.tapPromise()). However, they run each async method in parallel.
应用
注册事件有3个方法:tap , tapAsync , tapPromise ,其中以 Sync 开头的钩子的回调事件只能用 tap 来注册,否则会报错。
const { SyncHook } = require('tapable');
const hook = new SyncHook();
hook.tap('first', () => {
console.log('first');
});
hook.call();
可调整执行顺序
-
stage。值是数字类型,数字越大,事件回调执行时间越晚。// 代码来自 // Webpack 核心库 Tapable 的使用与原理解析 // https://juejin.cn/post/6844904037624578061#heading-21 const { SyncHook } = require('tapable'); const hook = new SyncHook(); hook.tap('first', () => { console.log('first'); }); hook.tap({ name: 'second', // 默认 stage 是 0,会按注册顺序添加事件回调到队列尾部 // 顺序提前,stage 可以置为负数(比零小) // 顺序提后,stage 可以置为正数(比零大) stage: 10, }, () => { console.log('second'); }); hook.tap('third', () => { console.log('third'); }); hook.call('call'); /** * Console output: * * first * third * second */ -
before。值类型是数组或字符串。值是注册事件回调的名称。// 代码来自 // Webpack 核心库 Tapable 的使用与原理解析 // https://juejin.cn/post/6844904037624578061#heading-21 const { SyncHook } = require('tapable'); const hook = new SyncHook(); hook.tap('first', (name) => { console.log('first', name); }); hook.tap('second', (name) => { console.log('second', name); }); hook.tap({ name: 'third', // 把 third 事件回调放到 second 之前执行 before: 'second', }, (name) => { console.log('third', name); }); hook.call('call'); /** * Console output: * * first * third * second */
触发事件
触发事件的方法跟注册事件的方法一一对应,call , callAsync ,promise 。
触发事件方法设置要跟注册事件的方法对应起来,不要混用。
触发事件方法的参数需要跟实例化时传给钩子类构造函数的数组长度保持一致,也要跟注册的回调事件的参数保持一致。
-
call// 代码来自 // Webpack 核心库 Tapable 的使用与原理解析 // https://juejin.cn/post/6844904037624578061#heading-21 const { SyncHook } = require('tapable'); // 1.实例化钩子类时传入的数组,实际上只用上了数组的长度,名称是为了便于维护 const hook = new SyncHook(['name']); // 3.other 会是 undefined,因为这个参数并没有在实例化钩子类的数组中声明 hook.tap('first', (name, other) => { console.log('first', name, other); }); // 2.实例化钩子类的数组长度为 1,这里却传了 2 个传入参数 hook.call('call', 'test'); /** * Console output: * * first call undefined */ -
callAsynccallAsync跟call不同的是,在回调事件参数的末尾,会多一个callback的事件,且callback是一定会执行的,否则,将不会执行之后的回调事件。且,如果callback中传递参数的话,将会阻断执行后面的回调事件,进入到callAsync的回调事件中。// 代码来自 // Webpack 核心库 Tapable 的使用与原理解析 // https://juejin.cn/post/6844904037624578061#heading-21 const { AsyncSeriesHook } = require('tapable'); const hook = new AsyncSeriesHook(['name']); hook.tapAsync('first', (name, callback) => { console.log('first', name, callback); callback(); }); hook.tapAsync('second', (name, callback) => { console.log('second', name, callback); callback('second'); }); hook.tapAsync('thrid', (name, callback) => { console.log('thrid', name, callback); callback('thrid'); }); hook.callAsync('callAsync', (error, result) => { console.log('callAsync', error, result); }); /** * first callAsync [Function] * second callAsync [Function] * callAsync second undefined */ -
promise使用
tapPromise注册事件回调时,事件执行后必须返回一个promise,否则就会报错,这是为了确保事件回调能够按照顺序执行。// 代码来自 // Webpack 核心库 Tapable 的使用与原理解析 // https://juejin.cn/post/6844904037624578061#heading-21 const { AsyncSeriesHook, AsyncSeriesWaterfallHook } = require('tapable'); // AsyncSeriesHook 是基础类型,不关心返回值,所以即使有返回值,也不会传递给下一个回调事件的 const hook = new AsyncSeriesHook(['name']); // AsyncSeriesWaterfallHook 代代相传 // const hook = new AsyncSeriesWaterfallHook(['name']); hook.tapPromise('first', (name) => { console.log('first', name); return Promise.resolve('first'); }); hook.tapPromise('second', (name) => { console.log('second', name); return Promise.resolve('second'); }); const promise = hook.promise('promise'); console.log(promise); promise.then(value => { // value 是 undefined,不会接收到事件回调中传入的值 console.log('value', value); }, reason => { // 事件回调返回的 Promise 对象状态是 Rejected // reason 会有事件回调中传入的错误信息 console.log('reason', reason); }); /** * AsyncSeriesHook Console output: * * first promise * Promise { <pending> } * second promise * value undefined */ /** * AsyncSeriesWaterfallHook Console output: * * first promise * Promise { <pending> } * second firse * value second */
以上触发回调的三种方法,处理返回值的方法也不一样:
call,return value;callAsync,callback('something');promise,promise.resolve('something');
暂缺拦截器和上下文的知识,待补上。
更多的对源码部分的解释,可阅读 Webpack 核心库 Tapable 的使用与原理解析 获得。