tapable 是一个类似于 Node.js 中的 EventEmitter 的库,但更专注于自定义事件的触发和处理。webpack 通过 tapable 将实现与流程解耦,所有具体实现通过插件的形式存在。

Tapable 和 webpack 的关系


webpack 是什么?

本质上,webpack 是一个用于现代 JavaScript 应用程序的 静态模块打包工具。当 webpack 处理应用程序时,它会在内部构建一个 依赖图(dependency graph),此依赖图对应映射到项目所需的每个模块,并生成一个或多个 bundle。

webpack 的重要模块

  • 入口(entry)
  • 输出(output)
  • loader(对模块的源代码进行转换)
  • plugin(webpack 构建流程中的特定时机注入扩展逻辑来改变构建结果或做你想要的事)

插件(plugin)是 webpack 的支柱功能。webpack 自身也是构建于你在 webpack 配置中用到的相同的插件系统之上。

webpack 的构建流程


webpack 本质上是一种事件流的机制,它的工作流程就是将各个插件串联起来,而实现这一切的核心就是 Tapable。webpack 中最核心的负责编译的 Compiler 和负责创建 bundle 的 Compilation 都是 Tapable 的实例(webpack5 前)。webpack5 之后是通过定义属性名为 hooks 来调度触发时机。Tapable 充当的就是一个复杂的发布订阅者模式

tabable的工作流程


实例化hook注册事件监听

  1. const { SyncHook } = require('tapable');
  2. // 实例化hook
  3. const hook = new SyncHook(['name', 'age']);
  4. hook.tap('1', (name, age) => {
  5. console.log('1===>', name, age);
  6. });
  7. //注册事件监听
  8. hook.tap('2', (name, age) => {
  9. console.log('2===>', name, age);
  10. });
  11. hook.tap('3', (name, age) => {
  12. console.log('3===>', name, age);
  13. });

通过hooke触发事件监听

  1. // 触发事件监听
  2. hook.call('zce', 100);

执行懒编译生成的可执行代码

Hooke


tapable 对外暴露了 9 种 Hooks 类。这些 Hooks 类的作用就是通过实例化来创建一个执行流程,并提供注册和执行方法,Hook 类的不同会导致执行流程的不同。
分未同步和异步,异步又分为并行和串行两种模式

  1. const {
  2. SyncHook,
  3. SyncBailHook,
  4. SyncWaterfallHook,
  5. SyncLoopHook,
  6. AsyncParallelHook,
  7. AsyncParallelBailHook,
  8. AsyncSeriesHook,
  9. AsyncSeriesBailHook,
  10. AsyncSeriesWaterfallHook
  11. } = require("tapable");

分类

按同步、异步(串行、并行)分类

  • Sync:只能被同步函数注册,如 myHook.tap()
  • AsyncSeries:可以被同步的,基于回调的,基于 promise 的函数注册,如 myHook.tap(),myHook.tapAsync() , myHook.tapPromise()。执行顺序为串行
  • AsyncParallel:可以被同步的,基于回调的,基于 promise 的函数注册,如 myHook.tap(),myHook.tapAsync() , myHook.tapPromise()。执行顺序为并行

image.png

按执行模式分类

  • Basic:执行每一个事件函数,不关心函数的返回值
    • image.png
  • Bail:执行每一个事件函数,遇到第一个结果 result !== undefined 则返回,不再继续执行
    • image.png
  • Waterfall:如果前一个事件函数的结果 result !== undefined,则 result 会作为后一个事件函数的第一个参数
  • image.png
  • Loop:不停的循环执行事件函数,直到所有函数结果 result === undefined
  • image.png

image.png

同步钩子

注册和回调

注册 执行
tap call

SyncHook

  1. const { SyncHook } = require('tapable');
  2. let hook = new SyncHook(['name', 'age']);
  3. hook.tap('fn1', function (name, age) {
  4. console.log('fn1--->', name, age);
  5. });
  6. hook.tap('fn2', function (name, age) {
  7. console.log('fn2--->', name, age);
  8. });
  9. hook.call('zoe', 18);

image.png

SynBailHook

如果中间有值返回了值,一旦有地方断掉了,后面就不会执行
尝试咋fn2返回非undefined

  1. const { SyncBailHook } = require('tapable');
  2. let hook = new SyncBailHook(['name', 'age']);
  3. hook.tap('fn1', function (name, age) {
  4. console.log('fn1--->', name, age);
  5. });
  6. hook.tap('fn2', function (name, age) {
  7. console.log('fn2--->', name, age);
  8. return 1;
  9. });
  10. hook.tap('fn3', function (name, age) {
  11. console.log('fn3--->', name, age);
  12. });
  13. hook.call('lg', 100);

image.png

当fn2返回了一个undefined的时候后面的fn3就会继续执行了

  1. const { SyncBailHook } = require('tapable');
  2. let hook = new SyncBailHook(['name', 'age']);
  3. hook.tap('fn1', function (name, age) {
  4. console.log('fn1--->', name, age);
  5. });
  6. hook.tap('fn2', function (name, age) {
  7. console.log('fn2--->', name, age);
  8. return undefined;
  9. });
  10. hook.tap('fn3', function (name, age) {
  11. console.log('fn3--->', name, age);
  12. });
  13. hook.call('lg', 100);

image.png

SynWaterfallHook

  1. const { SyncWaterfallHook } = require('tapable');
  2. let hook = new SyncWaterfallHook(['name', 'age']);
  3. hook.tap('fn1', function (name, age) {
  4. console.log('fn1--->', name, age);
  5. return 'ret1';
  6. });
  7. hook.tap('fn2', function (name, age) {
  8. console.log('fn2--->', name, age);
  9. return 'ret2';
  10. });
  11. hook.tap('fn3', function (name, age) {
  12. console.log('fn3--->', name, age);
  13. return 'ret3';
  14. });
  15. hook.call('zce', 33);

image.png

如果中间有值返回了undefined,name就继续传递上一个的回调

  1. const { SyncWaterfallHook } = require('tapable');
  2. let hook = new SyncWaterfallHook(['name', 'age']);
  3. hook.tap('fn1', function (name, age) {
  4. console.log('fn1--->', name, age);
  5. return 'fn1的回调';
  6. });
  7. hook.tap('fn2', function (name, age) {
  8. console.log('fn2--->', name, age);
  9. return undefined;
  10. });
  11. hook.tap('fn3', function (name, age) {
  12. console.log('fn3--->', name, age);
  13. return 'ret3';
  14. });
  15. hook.call('zce', 33);

image.png

SynLoopHook

  1. const { SyncLoopHook } = require('tapable');
  2. let hook = new SyncLoopHook(['name', 'age']);
  3. let count1 = 0;
  4. let count2 = 0;
  5. let count3 = 0;
  6. hook.tap('fn1', function (name, age) {
  7. console.log('fn1--->', name, age);
  8. if (++count1 === 1) {
  9. count1 = 0;
  10. return undefined;
  11. }
  12. return true;
  13. });
  14. hook.tap('fn2', function (name, age) {
  15. console.log('fn2--->', name, age);
  16. if (++count2 === 2) {
  17. count2 = 0;
  18. return undefined;
  19. }
  20. return true;
  21. });
  22. hook.tap('fn3', function (name, age) {
  23. console.log('fn3--->', name, age);
  24. });
  25. hook.call('foo', 100);

image.png

只要某一个环节返回的不是undefined,那么就从这个环节开始循环

异步串行钩子

注册和回调

异步钩子对于注册有两种方式

注册 执行 参数
tapAsync callAsync callback(err first)
tapPromise promise resolve/reject

tapAsync

  1. const { AsyncParallelHook } = require('tapable');
  2. let hook = new AsyncParallelHook(['name']);
  3. console.time('time');
  4. hook.tapAsync('fn1', function (name, callback) {
  5. setTimeout(() => {
  6. console.log('fn1--->', name);
  7. callback();
  8. }, 1000);
  9. });
  10. hook.tapAsync('fn2', function (name, callback) {
  11. setTimeout(() => {
  12. console.log('fn2--->', name);
  13. callback();
  14. }, 1000);
  15. });
  16. hook.callAsync('zce', function (err) {
  17. console.log(err);
  18. console.log(name);
  19. console.log('最后的回调执行了');
  20. console.timeEnd('time');
  21. });

在callback中传参就是error

  1. const { AsyncParallelHook } = require('tapable');
  2. let hook = new AsyncParallelHook(['name']);
  3. console.time('time');
  4. hook.tapAsync('fn1', function (name, callback) {
  5. setTimeout(() => {
  6. console.log('fn1--->', name);
  7. callback(1);
  8. }, 1000);
  9. });
  10. hook.tapAsync('fn2', function (name, callback) {
  11. setTimeout(() => {
  12. console.log('fn2--->', name);
  13. callback();
  14. }, 1000);
  15. });
  16. hook.callAsync('zce', function (err) {
  17. console.log(err);
  18. console.log('最后的回调执行了');
  19. console.timeEnd('time');
  20. });

image.png
注意此时
此时能够抛出fn2是因为callback发出err是在定时器当中,当发出callbackError的时候定时器已经注册了
如果改成如下

  1. const { AsyncParallelHook } = require('tapable');
  2. let hook = new AsyncParallelHook(['name']);
  3. console.time('time');
  4. hook.tapAsync('fn1', function (name, callback) {
  5. callback(1);
  6. setTimeout(() => {
  7. console.log('fn1--->', name);
  8. }, 1000);
  9. });
  10. hook.tapAsync('fn2', function (name, callback) {
  11. setTimeout(() => {
  12. console.log('fn2--->', name);
  13. callback();
  14. }, 1000);
  15. });
  16. hook.callAsync('zce', function (err) {
  17. console.log(err);
  18. console.log('最后的回调执行了');
  19. console.timeEnd('time');
  20. });

image.png
就不会触发fn2
promise

  1. const { AsyncParallelHook } = require('tapable');
  2. let hook = new AsyncParallelHook(['name']);
  3. console.time('time');
  4. hook.tapPromise('fn1', function (name) {
  5. return new Promise(function (resolve, reject) {
  6. setTimeout(() => {
  7. console.log('fn1--->', name);
  8. resolve();
  9. }, 1000);
  10. });
  11. });
  12. hook.tapPromise('fn2', function (name) {
  13. return new Promise(function (resolve, reject) {
  14. setTimeout(() => {
  15. console.log('fn2--->', name);
  16. resolve();
  17. }, 2000);
  18. });
  19. });
  20. hook.promise('foo').then(() => {
  21. console.log('end执行了');
  22. console.timeEnd('time');
  23. });

AsyncSeriesHook

钩子函数会串行执行,会保证执行的顺序,上一个钩子结束后,下一个钩子才会开始,执行事件注册的回调函数会最后执行。

  1. const hook = AsyncSeriesHook(['arg']);
  2. const start = Date.now();
  3. hook.tapAsync('listen1', (arg, callback) => {
  4. console.log(`listen1==耗时:${Date.now() - start}`)
  5. setTimeout(() => {
  6. callback();
  7. }, 1000);
  8. });
  9. hook.tapAsync('listen2', (arg, callback) => {
  10. console.log(`listen2==耗时:${Date.now() - start}`)
  11. setTimeout(() => {
  12. callback();
  13. }, 2000);
  14. });
  15. hook.callAsync('hello', () => {
  16. console.log(`回调函数执行,耗时:${Date.now() - start}`);
  17. });
  18. /**
  19. 输出:
  20. listen1==耗时:1
  21. listen2==耗时:1013
  22. 回调函数执行,耗时:3018
  23. */

AsyncSeriesBailHook

钩子函数会异步串行执行,但只要有一个钩子函数调用callback时传入了一个非undefined值,那么执行事件时注册的回调函数就会执行,剩下的钩子函数将不会执行。

  1. const hook = AsyncSeriesBailHook(['arg']);
  2. const start = Date.now();
  3. hook.tapAsync('listen1', (arg, callback) => {
  4. console.log('listen1')
  5. setTimeout(() => {
  6. callback(true);
  7. }, 1000);
  8. });
  9. hook.tapAsync('listen2', (arg, callback) => {
  10. console.log('listen2')
  11. setTimeout(() => {
  12. callback();
  13. }, 2000);
  14. });
  15. hook.callAsync('hello', () => {
  16. console.log(`回调函数执行,耗时:${Date.now() - start}`);
  17. });
  18. /**
  19. 输出:
  20. listen1
  21. 回调函数执行,耗时:1014
  22. */

AsyncSeriesWaterfallHookHook

钩子函数会异步串行执行,前一个钩子函数通过调用callback传入的参数会作为下一个钩子函数的参数,当所有钩子函数执行结束后,才会触发执行事件时注册的回调函数,该回调函数中接收了最后一个钩子函数返回的参数。

  1. const hook = AsyncSeriesWaterfallHook(['arg']);
  2. const start = Date.now();
  3. hook.tapAsync('listen1', (arg, callback) => {
  4. console.log('listen1', arg)
  5. setTimeout(() => {
  6. callback(null, `${arg} listen1`);
  7. }, 1000);
  8. });
  9. hook.tapAsync('listen2', (arg, callback) => {
  10. console.log('listen2', arg)
  11. setTimeout(() => {
  12. callback(null, `${arg} listen2`);
  13. }, 2000);
  14. });
  15. hook.callAsync('hello', (_, arg) => {
  16. console.log(`回调函数执行,耗时:${Date.now() - start}, arg:`, arg);
  17. });
  18. /**
  19. 输出:
  20. listen1 hello
  21. listen2 hello listen1
  22. 回调函数执行,耗时:3016, arg: hello listen1 listen2
  23. */

异步并行钩子

AsyncParallelHook

钩子函数会异步并行执行,当所有钩子函数的回调函数都执行后,才会触发执行事件时注册的回调函数。

  1. const hook = AsyncParallelHook(['arg']);
  2. const start = Date.now();
  3. hook.tapAsync('listen1', (arg, callback) => {
  4. console.log('listen1', arg)
  5. setTimeout(() => {
  6. callback();
  7. }, 1000);
  8. });
  9. hook.tapAsync('listen2', (arg, callback) => {
  10. console.log('listen2', arg);
  11. setTimeout(() => {
  12. callback();
  13. }, 2000);
  14. });
  15. hook.callAsync('hello', () => {
  16. console.log(`回调函数执行,耗时:${Date.now() - start}`);
  17. });
  18. /**
  19. 输出:
  20. listen1 hello
  21. listen2 hello
  22. 回调函数执行,耗时:2013
  23. */

callback传递一个err的时候回立即调用回调

  1. const hook = AsyncParallelHook(['arg']);
  2. const start = Date.now();
  3. hook.tapAsync('listen1', (arg, callback) => {
  4. console.log('listen1', arg)
  5. setTimeout(() => {
  6. callback(1);
  7. }, 1000);
  8. });
  9. hook.tapAsync('listen2', (arg, callback) => {
  10. console.log('listen2', arg);
  11. setTimeout(() => {
  12. callback();
  13. }, 2000);
  14. });
  15. hook.callAsync('hello', (err) => {
  16. console.log(err)
  17. console.log(`回调函数执行,耗时:${Date.now() - start}`);
  18. });
  19. /**
  20. 输出:
  21. listen1 hello
  22. 1
  23. 回调函数执行,耗时:2013
  24. */

AsyncParallelBailHook

钩子函数会异步并行执行,当某个钩子函数中调用callback时,传入了一个非undefined的值,那么执行事件时注册的回调函数会立即执行。

  1. const { AsyncParallelBailHook } = require('tapable');
  2. const hook = AsyncParallelBailHook(['arg']);
  3. const start = Date.now();
  4. hook.tapAsync('listen1', (arg, callback) => {
  5. setTimeout(() => {
  6. console.log('listen1', arg);
  7. callback(true);
  8. }, 1000);
  9. });
  10. hook.tapAsync('listen2', (arg, callback) => {
  11. setTimeout(() => {
  12. console.log('listen2', arg);
  13. callback();
  14. }, 2000);
  15. });
  16. hook.callAsync('hello', () => {
  17. console.log(`回调函数执行,耗时:${Date.now() - start}`);
  18. });
  19. /**
  20. 输出:
  21. listen1 hello
  22. 回调函数执行,耗时:1015
  23. listen2 hello
  24. */

Hook 类使用


Hook类使用

简单来说就是下面步骤

  1. 实例化构造函数 Hook
  2. 注册(一次或者多次)
  3. 执行(传入参数)
  4. 如果有需要还可以增加对整个流程(包括注册和执行)的监听-拦截器

以最简单的 SyncHook 为例:

  1. // 简单来说就是实例化 Hooks 类
  2. // 接收一个可选参数,参数是一个参数名的字符串数组
  3. const hook = new SyncHook(["arg1", "arg2", "arg3"]);
  4. // 注册
  5. // 第一个入参为注册名
  6. // 第二个为注册回调方法
  7. hook.tap("1", (arg1, arg2, arg3) => {
  8. console.log(1, arg1, arg2, arg3);
  9. return 1;
  10. });
  11. hook.tap("2", (arg1, arg2, arg3) => {
  12. console.log(2, arg1, arg2, arg3);
  13. return 2;
  14. });
  15. hook.tap("3", (arg1, arg2, arg3) => {
  16. console.log(3, arg1, arg2, arg3);
  17. return 3;
  18. });
  19. // 执行
  20. // 执行顺序则是根据这个实例类型来决定的
  21. hook.call("a", "b", "c");
  22. //------输出------
  23. // 先注册先触发
  24. 1 a b c
  25. 2 a b c
  26. 3 a b c

上面的例子为同步的情况,若注册异步则:

  1. let { AsyncSeriesHook } = require("tapable");
  2. let queue = new AsyncSeriesHook(["name"]);
  3. console.time("cost");
  4. queue.tapPromise("1", function (name) {
  5. return new Promise(function (resolve) {
  6. setTimeout(function () {
  7. console.log(1, name);
  8. resolve();
  9. }, 1000);
  10. });
  11. });
  12. queue.tapPromise("2", function (name) {
  13. return new Promise(function (resolve) {
  14. setTimeout(function () {
  15. console.log(2, name);
  16. resolve();
  17. }, 2000);
  18. });
  19. });
  20. queue.tapPromise("3", function (name) {
  21. return new Promise(function (resolve) {
  22. setTimeout(function () {
  23. console.log(3, name);
  24. resolve();
  25. }, 3000);
  26. });
  27. });
  28. queue.promise("weiyi").then((data) => {
  29. console.log(data);
  30. console.timeEnd("cost");
  31. });

HookMap 类使用

官方推荐将所有的钩子实例化在一个类的属性 hooks 上,如:

  1. class Car {
  2. constructor() {
  3. this.hooks = {
  4. accelerate: new SyncHook(["newSpeed"]),
  5. brake: new SyncHook(),
  6. calculateRoutes: new AsyncParallelHook(["source", "target", "routesList"])
  7. };
  8. }
  9. /* ... */
  10. setSpeed(newSpeed) {
  11. // following call returns undefined even when you returned values
  12. this.hooks.accelerate.call(newSpeed);
  13. }
  14. }

执行和注册

  1. const myCar = new Car();
  2. myCar.hooks.accelerate.tap("LoggerPlugin", newSpeed => console.log(`Accelerating to ${newSpeed}`));
  3. myCar.setSpeed(1)

而 HookMap 正是这种推荐写法的一个辅助类。具体使用方法:

  1. const keyedHook = new HookMap(key => new SyncHook(["arg"]))
  2. keyedHook.for("some-key").tap("MyPlugin", (arg) => { /* ... */ });
  3. keyedHook.for("some-key").tapAsync("MyPlugin", (arg, callback) => { /* ... */ });
  4. keyedHook.for("some-key").tapPromise("MyPlugin", (arg) => { /* ... */ });
  5. const hook = keyedHook.get("some-key");
  6. if(hook !== undefined) {
  7. hook.callAsync("arg", err => { /* ... */ });
  8. }

MultiHook 类使用

相当于提供一个存放一个 hooks 列表的辅助类:

  1. const { MultiHook } = require("tapable");
  2. this.hooks.allHooks = new MultiHook([this.hooks.hookA, this.hooks.hookB]);