tapable 是 webpack 的核心工具库。

工作流程

  1. 实例化 hook 注册事件监听
  2. 通过 hook 触发事件监听
  3. 运行懒编译的代码

Hook 分类

根据执行机制,hook 分为同步(Sync)和异步(Async)。异步 hook 根据执行方式又可以分为串行执行(Series)和并行执行(Parallel)。
根据执行特点,hook 又可以分为以下四类:

  • Hook : 普通钩子,监听器之间互相不干扰。
  • BailHook : 熔断钩子,某个监听返回非 undefined 时后续不执行。
  • WaterfallHook : 瀑布钩子,上一个监听的返回值可以传递给下一个。
  • LoopHook : 循环钩子,如果当前未返回 false 则一直执行。

hook.svg

同步钩子工作流程概述

  1. 导入钩子
    1. 导入钩子时,会自动创建一个对应的代码工厂 HookCodeFactory。
  2. 实例化钩子实例
    1. 创建一个 Hook 实例,传入形参,挂载args、taps、_x属性,重写构造函数,tap 方法,compile 方法,返回 hook 实例。
  3. 使用 tap 注册函数
    1. 把函数名、函数体合并成一个对象,将对象放入 taps 数组中。
  4. 触发事件
    1. 调用 call 方法,调用重写过的 compile 方法,
    2. 从 taps 数组中取出所有回调函数体,放入 _x 数组中。
    3. 从 _x 数组中取出事件函数,拼接成一段可执行代码。
    4. 传入实参到生成的可执行代码的自调用函数。

同步钩子源码实现

Hook类:

  1. class Hook {
  2. constructor(args = []) {
  3. this.args = args
  4. this.taps = [] // 将来用于存放组装好的 {}
  5. this._x = undefined // 将来在代码工厂函数中会给 _x = [f1, f2, ...]
  6. }
  7. tap(options, fn) {
  8. if (typeof options === 'string') {
  9. options = { name: options }
  10. }
  11. options = Object.assign({fn}, options) // { fn: func, name: fn1 }
  12. // 调用一下方法将组装好的 options 添加至 []
  13. this._insert(options)
  14. }
  15. _insert(options) {
  16. this.taps[this.taps.length] = options
  17. }
  18. call(...args) {
  19. // 01 创建将来要具体执行的函数代码结构
  20. let callFn = this._createCall()
  21. // 02 调用上述的函数(args传入进去)
  22. return callFn.apply(this, args)
  23. }
  24. _createCall() {
  25. return this.compile({
  26. taps: this.taps,
  27. args: this.args
  28. })
  29. }
  30. }
  31. module.exports = Hook

SyncHook类和代码工厂类:

  1. const Hook = require('./Hook')
  2. class HookCodeFactory {
  3. args() {
  4. return this.options.args.join(',') // ['name', 'age'] -> 'name,age'
  5. }
  6. head() {
  7. return `var _x = this._x;`
  8. }
  9. content() {
  10. let code = ''
  11. for (let i = 0; i < this.options.taps.length; i++) {
  12. // code += `var _fn0 = _x[0];_fn0(name, age)`
  13. code += `var _fn${i} = _x[${i}];_fn${i}(${this.args()});`
  14. }
  15. return code
  16. }
  17. setup(instance, options) { // 先准备后续需要使用的数据
  18. this.options = options // 这里的操作在源码中是通过 init 方法实现,目前只是直接挂载在 this 上
  19. instance._x = options.taps.map(t => t.fn) // this._x = [f1, f2,...]
  20. }
  21. create() { // 核心是创建一段可执行的代码体然后返回
  22. let fn
  23. fn = new Function(this.args(), this.head() + this.content())
  24. return fn
  25. }
  26. }
  27. let factory = new HookCodeFactory()
  28. class SyncHook extends Hook {
  29. constructor(args) {
  30. super(args)
  31. }
  32. compile(options) { // options: { taps: [{}, {}...], args: [name, age] }
  33. factory.setup(this, options)
  34. return factory.create(options,)
  35. }
  36. }
  37. module.exports = SyncHook

使用自实现 SyncHook :

  1. const SyncHook = require('./SyncHook')
  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('zce', 18)

异步钩子工作流程概述

异步钩子工作流程大致与同步钩子相同,主要有两个区别:

  1. 拼接代码时,代码工厂类的 args 方法传入了 after 参数,值为 ‘_callback’,这样一来生成的代码的自调用函数就会多一个形参 _callback。
  2. 生成的自调用函数有一个计数器 _counter,一个最后执行的 done 方法,done 方法调用 _callback 参数传进来的函数。

异步钩子源码实现

Hook 类:

  1. class Hook {
  2. constructor(args = []) {
  3. this.args = args
  4. this.taps = [] // 将来用于存放组装好的 {}
  5. this._x = undefined // 将来在代码工厂函数中会给 _x = [f1, f2, ...]
  6. }
  7. tapAsync(options, fn) {
  8. if (typeof options === 'string') {
  9. options = { name: options }
  10. }
  11. options = Object.assign({fn}, options) // { fn: func, name: fn1 }
  12. // 调用一下方法将组装好的 options 添加至 []
  13. this._insert(options)
  14. }
  15. _insert(options) {
  16. this.taps[this.taps.length] = options
  17. }
  18. callAsync(...args) {
  19. // 01 创建将来要具体执行的函数代码结构
  20. let callFn = this._createCall()
  21. // 02 调用上述的函数(args传入进去)
  22. return callFn.apply(this, args)
  23. }
  24. _createCall() {
  25. return this.compile({
  26. taps: this.taps,
  27. args: this.args
  28. })
  29. }
  30. }
  31. module.exports = Hook
  1. AsyncParallelHook 工厂类:
  1. const Hook = require('./Hook')
  2. class HookCodeFactory {
  3. args({ before, after } = {}) {
  4. let allArgs = this.options.args
  5. if (before) allArgs = [before].concat(allArgs)
  6. if (after) allArgs = allArgs.concat(after)
  7. return allArgs.join(',')
  8. }
  9. head() {
  10. return `"use strict";var _x = this._x;var _content;`
  11. }
  12. content() {
  13. let code = `var _counter = ${this.options.taps.length}; var _done = (function () { _callback(); });`
  14. for (let i = 0; i < this.options.taps.length; i++) {
  15. code += `var _fn${i} = _x[${i}]; _fn${i}(${this.args()}, (function () { if (--_counter === 0) _done(); }));`
  16. }
  17. return code
  18. }
  19. setup(instance, options) { // 先准备后续需要使用的数据
  20. this.options = options // 这里的操作在源码中是通过 init 方法实现,目前只是直接挂载在 this 上
  21. instance._x = options.taps.map(t => t.fn) // this._x = [f1, f2,...]
  22. }
  23. create() { // 核心是创建一段可执行的代码体然后返回
  24. let fn
  25. fn = new Function(this.args({ after: '_callback' }), this.head() + this.content())
  26. return fn
  27. }
  28. }
  29. let factory = new HookCodeFactory()
  30. class AsyncParallelHook extends Hook {
  31. constructor(args) {
  32. super(args)
  33. }
  34. compile(options) { // options: { taps: [{}, {}...], args: [name, age] }
  35. factory.setup(this, options)
  36. return factory.create(options)
  37. }
  38. }
  39. module.exports = AsyncParallelHook

使用自实现 AsyncParallelHook:

  1. const AsyncParallelHook = require('./AsyncParallelHook')
  2. let hook = new AsyncParallelHook(['name', 'age'])
  3. hook.tapAsync('fn1', function (name, age, callback) {
  4. console.log('fn1--->', name, age)
  5. callback()
  6. })
  7. hook.tapAsync('fn2', function (name, age, callback) {
  8. console.log('fn2--->', name, age)
  9. callback()
  10. })
  11. hook.callAsync('zce', 18, function () {
  12. console.log('end')
  13. })