Webpack4源码解析之Tapable

257 阅读8分钟

Webpack源码解析

// 使用webpack版本

"html-webpack-plugin": "^4.5.0",
"webpack": "^4.44.2",
"webpack-cli": "^3.3.12"

Webpack 与 Tapable

Webpack的构建过程分为配置初始化、内容编译、输出编译后的内容,这三个过程可以看做成一个事件驱动型的事件流工作机制,通过事件将一个个构建任务串联起来,而实现这一切的就是 Tapable。Webpack 的两大核心模块负责编译的 Compiler 和负责创建bundles的 Compilation 都是 Tapable 的子类,并且实例内部的生命周期也是通过 Tapable 库提供的钩子类实现的:

class Compiler extends Tapable {
  constructor(context) {
    super();
    this.hooks = {
      shouldEmit: new SyncBailHook(["compilation"]),
      done: new AsyncSeriesHook(["stats"]),
      additionalPass: new AsyncSeriesHook([]),
      beforeRun: new AsyncSeriesHook(["compiler"]),
      run: new AsyncSeriesHook(["compiler"]),
      emit: new AsyncSeriesHook(["compilation"]),
      assetEmitted: new AsyncSeriesHook(["file", "content"]),
      afterEmit: new AsyncSeriesHook(["compilation"]),
      thisCompilation: new SyncHook(["compilation", "params"]),
      compilation: new SyncHook(["compilation", "params"]),
      normalModuleFactory: new SyncHook(["normalModuleFactory"]),
      contextModuleFactory: new SyncHook(["contextModulefactory"]),
      beforeCompile: new AsyncSeriesHook(["params"]),
      compile: new SyncHook(["params"]),
      make: new AsyncParallelHook(["compilation"]),
      afterCompile: new AsyncSeriesHook(["compilation"]),
      watchRun: new AsyncSeriesHook(["compiler"]),
      failed: new SyncHook(["error"]),
      invalid: new SyncHook(["filename", "changeTime"]),
      watchClose: new SyncHook([]),
      infrastructureLog: new SyncBailHook(["origin", "type", "args"]),
      environment: new SyncHook([]),
      afterEnvironment: new SyncHook([]),
      afterPlugins: new SyncHook(["compiler"]),
      afterResolvers: new SyncHook(["compiler"]),
      entryOption: new SyncBailHook(["context", "entry"]),
    };
  }
}

Tapable是什么

这个小型库是 webpack 的一个核心工具,但也可用于其他地方, 以提供类似的插件接口。 在 webpack 中的许多对象都扩展自 Tapable 类。 它对外暴露了 tap,tapAsync 和 tapPromise 等方法, 插件可以使用这些方法向 webpack 中注入自定义构建的步骤,这些步骤将在构建过程中触发。

首先 Tapable 是一个库,它是实现 webpack 插件化的核心,从 Compiler 内部可以看出 Tapable 提供的方法都是用于其生命周期实例化的,因此也可以称之为钩子类 HOOK

HOOK 本质是 Tapable 实例对象,它可以分为 同步钩子异步钩子 两大类,异步钩子又可以分为并行和串行两类。

Tapable导出的钩子类有:

const {
  SyncHook,
  SyncBailHook,
  SyncWaterfallHook,
  SyncLoopHook,
  AsyncSeriesHook,
  AsyncSeriesBailHook,
  AsyncSeriesWaterfallHook,
  AsyncParallelHook,
  AsyncParallelBailHook,
} = require('tapable');

hook按事件回调的运行机制分为四种:

  • Hook : 普通钩子 , 监听器之间互相独立不干扰
  • BailHook: 熔断钩子, 某个监听返回非 undefined 时后续不执行
  • WaterfallHook: 漫布钩子 , 如果上一个回调的返回值不为undefined,那么就将下一个的回调的第一个参数替换为这个值
  • LoopHook: 循环钩子 , 如果当前未返回 undefined 则一直执行

hook按触发事件的方式分:

类型描述
SyncSync开头的Hook类只能用 tap 方法注册事件回调,这类事件回调会同步执行;如果使用 tapAsync 或者 tapPromise 方法注册会报错,SyncHook重写了Hook
AsyncSeriesAsync 开头的 Hook 类,没法用 call 方法触发事件,必须用 callAsync 或者 promise 方法触发;这两个方法都能触发 taptapAsynctapPromise 注册的事件回调。AsyncSeries 按照顺序执行,当前事件回调如果是异步的,那么会等到异步执行完毕才会执行下一个事件回调。
AsyncParalleAsyncParalle 会并行执行所有的事件回调。

同步钩子的使用

1. SyncHook

基础钩子,并不关心回调的内部具体实现,只是单纯地执行回调:

// SyncHook.js

const { SyncHook } = require('tapable')

const hook = new SyncHook(['name', 'age'])

hook.tap('fn1', function(name, age) {
  console.log(`fn1 ---> ${name} is ${age} years old.`)
})

hook.tap('fn2', function(name, age) {
  console.log(`fn2 ---> ${name} is ${age} years old.`)
  return name + 'fun2'
})

hook.tap('fn3', function(name, age) {
  console.log(`fn3 ---> ${name} is ${age} years old.`)
})

hook.call('jack', 12)
fn1 ---> jack is 12 years old.
fn2 ---> jack is 12 years old.
fn3 ---> jack is 12 years old.

由此可见 BasicHook 的回调都是独立的,互不干扰。在注册事件回调时,配置对象有两个可以改变执行顺序的属性:

stage

stage 可以设置为一个数字,值越大执行顺序就越靠后:

// SyncHookStage.js

const { SyncHook } = require('tapable')

const hook = new SyncHook(['name', 'age'])

hook.tap('fn1', function(name, age) {
  console.log(`fn1 ---> ${name} is ${age} years old.`)
})

hook.tap({
  name: 'fn2',
  stage: 8,
}, function(name, age) {
  console.log(`fn2 ---> ${name} is ${age} years old.`)
})

hook.tap({
  name: 'fn3',
  stage: 6,
}, function(name, age) {
  console.log(`fn3 ---> ${name} is ${age} years old.`)
})

hook.tap('fn4', function(name, age) {
  console.log(`fn4 ---> ${name} is ${age} years old.`)
})

hook.call('jack', 12)
fn1 ---> jack is 12 years old.
fn4 ---> jack is 12 years old.
fn3 ---> jack is 12 years old.
fn2 ---> jack is 12 years old.

before

顾名思义,before 就是配置其在某个任务之前执行,它的值是一个字符串,是其它的任务名:

// SyncHookBefore.js

const { SyncHook } = require('tapable')

const hook = new SyncHook(['name', 'age'])

hook.tap('fn1', function(name, age) {
  console.log(`fn1 ---> ${name} is ${age} years old.`)
})

hook.tap('fn2', function(name, age) {
  console.log(`fn2 ---> ${name} is ${age} years old.`)
})

hook.tap({
  name: 'fn3',
  before: 'fn2'
}, function(name, age) {
  console.log(`fn3 ---> ${name} is ${age} years old.`)
})

hook.call('jack', 12)
fn1 ---> jack is 12 years old.
fn3 ---> jack is 12 years old.
fn2 ---> jack is 12 years old.

2. SyncBailHook

保险类型的钩子,当上一个回调函数的返回值不为 undefined 时,后续注册的回调函数不再执行:

// SyncBailHook.js

const { SyncBailHook } = require('tapable')

const hook = new SyncBailHook(['name', 'age'])

hook.tap('fn1', (name, age) => {
  console.log(`fn1 ---> ${name} is ${age} years old.`)
})

hook.tap('fn2', (name, age) => {
  console.log(`fn2 ---> ${name} is ${age} years old.`)
  return undefined
})

hook.tap('fn3', (name, age) => {
  console.log(`fn3 ---> ${name} is ${age} years old.`)
  return name + '3'
})

hook.tap('fn4', (name, age) => {
  console.log(`fn4 ---> ${name} is ${age} years old.`)
})

hook.call('tom', 8)
fn1 ---> tom is 8 years old.
fn2 ---> tom is 8 years old.
fn3 ---> tom is 8 years old.

3. SyncWaterfallHook

瀑布流hook,上一个函数的返回结果不为undefined,那就将其作为下一个函数的第一个参数:

// SyncWaterfallHook.js

const { SyncWaterfallHook } = require('tapable')

const hook = new SyncWaterfallHook(['name', 'age'])

hook.tap('fn1', (name, age) => {
  console.log(`fn1 ---> ${name} is ${age} years old.`)
})

hook.tap('fn2', (name, age) => {
  console.log(`fn2 ---> ${name} is ${age} years old.`)
  return 'jack'
})

hook.tap('fn3', (name, age) => {
  console.log(`fn3 ---> ${name} is ${age} years old.`)
  return 'lucy'
})

hook.tap('fn4', (name, age) => {
  console.log(`fn4 ---> ${name} is ${age} years old.`)
})

hook.call('tom', 8)
fn1 ---> tom is 8 years old.
fn2 ---> tom is 8 years old.
fn3 ---> jack is 8 years old.
fn4 ---> lucy is 8 years old.

即使返回一个错误,也会将错误的message作为参数:

// SyncWaterfallHook.js

...

hook.tap('fn3', (name, age) => {
  console.log(`fn3 ---> ${name} is ${age} years old.`)
  return new Error('fn3 error')
})

...
fn4 ---> Error: fn3 error is 8 years old.

4. SyncLoopHook

Loop 类型的钩子类在当前执行的事件回调的返回值不是 undefined 时,会重新从第一个注册的事件回调处执行,直到当前执行的事件回调没有返回值才会执行后面的回调函数:

// SyncLoopHook.js

const { SyncLoopHook } = require('tapable')

const hook = new SyncLoopHook(['name', 'age'])

let count1 = 0
let count2 = 0
let count3 = 0

hook.tap('fn1', (name, age) => {
  console.log(`fn1 ---> ${name} is ${age} years old.`)
  if (++count1 === 1) {
    count1 = 0
    return undefined
  }
  return true
})

hook.tap('fn2', (name, age) => {
  console.log(`fn2 ---> ${name} is ${age} years old.`)
  if (++count2 === 2) {
    count2 = 0
    return undefined
  }
  return true
})

hook.tap('fn3', (name, age) => {
  console.log(`fn3 ---> ${name} is ${age} years old.`)
})

hook.call('lucy', 10)
fn1 ---> lucy is 10 years old.
fn2 ---> lucy is 10 years old.
fn1 ---> lucy is 10 years old.
fn2 ---> lucy is 10 years old.
fn3 ---> lucy is 10 years old.

hook.tap 注册回调事件时会调用hook的 _tap 方法,经过一些内部处理,将回调填入 tabs 中:

[
  {
    type: "sync",
    fn: (name, age) => {
      console.log(`fn1 ---> ${name} is ${age} years old.`)
      if (++count1 === 1) {
        count1 = 0
        return undefined
      }
      return true
    },
    name: "fn1",
  },
  {
    type: "sync",
    fn: (name, age) => {
      console.log(`fn2 ---> ${name} is ${age} years old.`)
      if (++count2 === 2) {
        count2 = 0
        return undefined
      }
      return true
    },
    name: "fn2",
  },
  {
    type: "sync",
    fn: (name, age) => {
      console.log(`fn3 ---> ${name} is ${age} years old.`)
    },
    name: "fn3",
  },
]

我们看一下 call 方法做了什么:

// Hook.js

const CALL_DELEGATE = function(...args) {
  this.call = this._createCall("sync");
  return this.call(...args);
};
// <anonymous> (<eval>/VM46947590:3)
// this.call 调用堆栈

(function anonymous(name, age) {
  "use strict";
  var _context;
  // tags 中的回调函数fn
  var _x = this._x;
  // 定义一个变量判断是否需要进行循环调用
  var _loop;
  do {
    // 初始值设为false
    _loop = false;
    // 取出第一个回调函数执行
    var _fn0 = _x[0];
    // 接收第一个函数返回值
    var _result0 = _fn0(name, age);
    // 如果第一个函数的返回值不为 undefined,就设置循环调用为 true
    // 继续调用第一个回调函数
    if (_result0 !== undefined) {
      _loop = true;
    } else {
      // 否则就取出第二个回调函数执行
      var _fn1 = _x[1];
      var _result1 = _fn1(name, age);
      // 第二个函数的返回结果不为 undefined,如上
      if (_result1 !== undefined) {
        _loop = true;
      } else {
        var _fn2 = _x[2];
        var _result2 = _fn2(name, age);
        if (_result2 !== undefined) {
          _loop = true;
        } else {
          if (!_loop) {
          }
        }
      }
    }
  } while (_loop);
});

从上面执行栈中的代码可以看出,标识 _loop 取决于上一个回调函数的执行结果,为 true 时不再执行后续的回调函数,而是循环执行上面的回调函数,直到返回结果为 undefined 为止。

异步钩子使用

异步钩子按照执行回调的机制分为 并行串行 两种,我们先看一下并行执行。对于异步钩子,添加事件监听的方式有三种:taptapAsynctapPromise

1. tap

// AsyncParallelHookTap.js

const { AsyncParallelHook } = require('tapable')

const hook = new AsyncParallelHook(['name'])

hook.tap('fn1', (name) => {
  console.log(`fn1 ---> ${name}`)
  console.log(Date.now())
})

hook.tap('fn2', (name) => {
  console.log(`fn2 ---> ${name}`)
  console.log(Date.now())
})

hook.callAsync('run', () => {
  console.log('all done.')
})
fn1 ---> run
1629700481997
fn2 ---> run
1629700481997
all done.

并行的异步钩子几乎是同时执行的,当所有回调函数执行完毕后会触发执行完毕的回调函数,且所有回调函数之间互不干扰。

2. tabAsync

tabAsync 通过添加回调函数的方式来表示当前回调执行完毕:

// AsyncParallelHookTapAsync.js

const { AsyncParallelHook } = require('tapable')

const hook = new AsyncParallelHook(['name'])

console.time('time')

hook.tapAsync('fn1', (name, callback) => {
  // console.log(callback.toString())
  // function (_err0) {
  //   if(_err0) {
  //     if(_counter > 0) {
  //       _callback(_err0);
  //       _counter = 0;
  //     }
  //   } else {
  //     if(--_counter === 0) _done();
  //   }
  // }
  setTimeout(() => {
    console.log(`fn1 ---> ${name}`)
    callback()
  }, 1000)
})

hook.tapAsync('fn2', (name, callback) => {
  // console.log(callback.toString())
  // function(_err1) {
  //   if(_err1) {
  //     if(_counter > 0) {
  //       _callback(_err1);
  //       _counter = 0;
  //     }
  //   } else {
  //     if(--_counter === 0) _done();
  //   }
  // }
  setTimeout(() => {
    console.log(`fn2 ---> ${name}`)
    callback()
  }, 2000)
})

hook.callAsync('run', () => {
  console.log('all done.')
  console.timeEnd('time')
})
fn1 ---> run
fn2 ---> run
all done.
time: 2.010s

如上所示,我们打印了这个回调函数,可以看出,当我们调用回调函数不传递错误信息时,并且调用量(回调函数总数)递减为0时确认所有回调函数调用完毕。

一旦我们在某个回调中传递了错误信息,则立即调用hook的回调,并将调用量置为0,终止下面回调执行:

// AsyncParallelHookTapAsync.js

...

setTimeout(() => {
  console.log(`fn1 ---> ${name}`)
  callback('err')
}, 1000)

...
fn1 ---> run
all done.
time: 1.011s

当第一个回调传递错误信息时,立即终止执行,所有回调执行完毕。

3. tapPromise

tapPromise 注册的回调必须返回一个 Promise,通过调用 resolve() 方法表示该回调函数执行完毕:

// AsyncParallelHookTapPromise.js

const { AsyncParallelHook } = require('tapable')

const hook = new AsyncParallelHook(['name'])

console.time('time')

hook.tapPromise('fn1', (name) => {
  return new Promise(resolve => {
    setTimeout(() => {
      console.log(`fn1 ---> ${name}`)
      resolve()
    }, 1000)
  })
})

hook.tapPromise('fn2', (name) => {
  return new Promise(resolve => {
    setTimeout(() => {
      console.log(`fn2 ---> ${name}`)
      resolve()
    }, 2000)
  })
})

hook.promise('run').then(() => {
  console.log('all done.')
  console.timeEnd('time')
})
fn1 ---> run
fn2 ---> run
all done.
time: 2.008s

tapPromise 注册的回调必须返回一个 Promise ,否则会报错,必须调用 resolve 方法标注该回调执行完毕,否则 hook 执行完毕的回调无法执行。如果某个回调调用了 reject,hook 的回调也不会执行。

以上就是三种添加异步回调的方法。

AsyncParallelBailHook 异步并行熔断钩子,和同步的熔断钩子类似,当回调函数的returncallback 的实参或者 resolve 的值不为 undefined 时,终止所有回调函数的执行,并调用 hook 的回调。

以上存在三种回调函数,这里不能弄混淆了,一个是我们自己注册的回调函数,用来处理我们自定义的任务;一个是标注我们自定义的函数执行完毕,成功与否通过回调实参表示;最后一个是所有回调都成功执行后的最终回调,只要有一个回调函数执行异常,标注了错误信息,那么这个最终回调就无法执行。

以上注册的回调函数都是并行的(同时执行),而串行执行就是注册的回调函数依次顺序执行,下面举一个例子看一下特点:

// AsyncSeriesHookTapAsync.js

const { AsyncSeriesHook } = require('tapable')

const hook = new AsyncSeriesHook(['name'])

console.time('time')

hook.tapAsync('fn1', (name, callback) => {
  setTimeout(() => {
    console.log(`fn1 ---> ${name}`)
    callback()
  }, 1000)
})

hook.tapAsync('fn2', (name, callback) => {
  setTimeout(() => {
    console.log(`fn2 ---> ${name}`)
    callback()
  }, 2000)
})

hook.callAsync('run', () => {
  console.log('all done.')
  console.timeEnd('time')
})
fn1 ---> run
fn2 ---> run
all done.
time: 3.015s

当使用 tapAsync 注册回调函数时,因为 callback 函数的特点,不管你使用的是熔断钩子还是非熔断钩子,它都会立即调用hook的回调,唯一区别就是串行后续回调函数不再执行,并行因为是同时执行的原因,后面的回调会执行,只是在标注错误信息的那一刻立即调用hook的最终回调。

以下是并行和串行使用callback的调用栈对比,可以看出不同的地方:

// parallel hook VM

(function anonymous(name, _callback) {
  "use strict";
  var _context;
  var _x = this._x;
  do {
    var _counter = 3;
    var _done = function () {
      _callback();
    };
    if (_counter <= 0) break;
    var _fn0 = _x[0];
    _fn0(name, function (_err0) {
      if (_err0) {
        if (_counter > 0) {
          _callback(_err0);
          _counter = 0;
        }
      } else {
        if (--_counter === 0) _done();
      }
    });
    if (_counter <= 0) break;
    var _fn1 = _x[1];
    _fn1(name, function (_err1) {
      if (_err1) {
        if (_counter > 0) {
          _callback(_err1);
          _counter = 0;
        }
      } else {
        if (--_counter === 0) _done();
      }
    });
    if (_counter <= 0) break;
    var _fn2 = _x[2];
    _fn2(name, function (_err2) {
      if (_err2) {
        if (_counter > 0) {
          _callback(_err2);
          _counter = 0;
        }
      } else {
        if (--_counter === 0) _done();
      }
    });
  } while (false);
});
// series hook VM

(function anonymous(name, _callback) {
  "use strict";
  var _context;
  var _x = this._x;
  function _next1() {
    var _fn2 = _x[2];
    _fn2(name, function (_err2) {
      if (_err2) {
        _callback(_err2);
      } else {
        _callback();
      }
    });
  }
  function _next0() {
    var _fn1 = _x[1];
    _fn1(name, function (_err1) {
      if (_err1) {
        _callback(_err1);
      } else {
        _next1();
      }
    });
  }
  var _fn0 = _x[0];
  _fn0(name, function (_err0) {
    if (_err0) {
      _callback(_err0);
    } else {
      _next0();
    }
  });
});

SyncHook源码解析

调试代码:

// SyncHook.js

const { SyncHook } = require('tapable')

const hook = new SyncHook(['name', 'age'])

hook.tap('fn1', function(name, age) {
  console.log(`fn1 ---> ${name} is ${age} years old.`)
})

hook.tap('fn2', function(name, age) {
  console.log(`fn2 ---> ${name} is ${age} years old.`)
})

hook.tap('fn3', function(name, age) {
  console.log(`fn3 ---> ${name} is ${age} years old.`)
})

hook.call('jack', 12)
// SyncHookBefore.js

const { SyncHook } = require('tapable')

const hook = new SyncHook(['name', 'age'])

hook.tap('fn0', function(name, age) {
  console.log(`fn0 ---> ${name} is ${age} years old.`)
})

hook.tap('fn1', function(name, age) {
  console.log(`fn1 ---> ${name} is ${age} years old.`)
})

hook.tap('fn2', function(name, age) {
  console.log(`fn2 ---> ${name} is ${age} years old.`)
})

hook.tap('fn3', function(name, age) {
  console.log(`fn3 ---> ${name} is ${age} years old.`)
})

hook.tap({
  name: 'fn4',
  before: 'fn2'
}, function(name, age) {
  console.log(`fn4 ---> ${name} is ${age} years old.`)
})

hook.call('jack', 12)
// SyncHookStage.js

const { SyncHook } = require('tapable')

const hook = new SyncHook(['name', 'age'])

hook.tap('fn1', function(name, age) {
  console.log(`fn1 ---> ${name} is ${age} years old.`)
})

hook.tap({
  name: 'fn2',
  stage: 8,
}, function(name, age) {
  console.log(`fn2 ---> ${name} is ${age} years old.`)
})

hook.tap({
  name: 'fn3',
  stage: 6,
}, function(name, age) {
  console.log(`fn3 ---> ${name} is ${age} years old.`)
})

hook.tap('fn4', function(name, age) {
  console.log(`fn4 ---> ${name} is ${age} years old.`)
})

hook.call('jack', 12)

1. 实例化SyncHook对象

const hook = new SyncHook(['name', 'age'])

SyncHook源码:

// SyncHook.js

/*
  MIT License http://www.opensource.org/licenses/mit-license.php
  Author Tobias Koppers @sokra
*/
"use strict";

// 加载Hook类
const Hook = require("./Hook");

// 加载Hook生成执行代码的工厂类(也就是生成上文中存在于调用栈中的VM- - - -)
const HookCodeFactory = require("./HookCodeFactory");

// 同步hook代码生成工厂类继承基础的hook代码生成工厂类
class SyncHookCodeFactory extends HookCodeFactory {
  /**
    * 定义成员方法,调用父类的callTapsSeries组装执行代码块
    * @param { 失败的回调,成功的回调,处理异常并重新抛出异常 } param0 
    * @returns code
    */
  content({ onError, onDone, rethrowIfPossible }) {
    return this.callTapsSeries({
      onError: (i, err) => onError(err),
      onDone,
      rethrowIfPossible
    });
  }
}

// 实例化code工厂类
const factory = new SyncHookCodeFactory();

// 重写Hook类的TAP_ASYNC方法,syncHook不能使用tapAsync调用
const TAP_ASYNC = () => {
  throw new Error("tapAsync is not supported on a SyncHook");
};

// 重新Hook类的TAP_PROMISE方法,syncHook不能使用tapPromise调用
const TAP_PROMISE = () => {
  throw new Error("tapPromise is not supported on a SyncHook");
};

/**
 * 编译代码(生成可执行代码)
 * @param { 参数选项 } options
 * {
 *  taps: this.taps,
 *  interceptors: this.interceptors,
 *  args: this._args,
 *  type: type
 * }
 */
const COMPILE = function(options) {
  // this对应hook实例,它有一个_x属性(this._x = undefined;)
  // 这一步的目的就是将taps中所有tap的fn取出来放到_x中
  factory.setup(this, options);
  // 这一步就是根据不同的type来创建执行函数
  return factory.create(options);
};

/**
 * SyncHook构造函数,new SyncHook时会调用它
 * @param {回调函数携带的参数} args 
 * @param {该syncHook命名} name 
 * @returns syncHook实例
 */
function SyncHook(args = [], name = undefined) {
  // 实例化一个基础的hook类
  const hook = new Hook(args, name);
  // 构造函数是SyncHook
  hook.constructor = SyncHook;
  // 重写hook的tapAsync、tapPromise、compile
  hook.tapAsync = TAP_ASYNC;
  hook.tapPromise = TAP_PROMISE;
  hook.compile = COMPILE;
  return hook;
}

// SyncHook的原型链指向null
SyncHook.prototype = null;

module.exports = SyncHook;

2. 实例化Hook,并调用hook实例的tap方法注册一个钩子函数

const hook = new Hook(args, name);
hook.tap('fn1', function(name, age) {
  console.log(`fn1 ---> ${name} is ${age} years old.`)
})

Hook.js源码

// Hook.js

/*
 MIT License http://www.opensource.org/licenses/mit-license.php
 Author Tobias Koppers @sokra
*/
"use strict";

const util = require("util");

// 新的版本已近弃用 Hook.context
const deprecateContext = util.deprecate(() => {},
"Hook.context is deprecated and will be removed");

// 异步钩子的三种调用方法,主要是为了标注调用方式,为了后面生成执行函数时可以通过 type 来进行判断
const CALL_DELEGATE = function (...args) {
  this.call = this._createCall("sync");
  return this.call(...args);
};
const CALL_ASYNC_DELEGATE = function (...args) {
  this.callAsync = this._createCall("async");
  return this.callAsync(...args);
};
const PROMISE_DELEGATE = function (...args) {
  this.promise = this._createCall("promise");
  return this.promise(...args);
};

class Hook {
  constructor(args = [], name = undefined) {
    // 接收注册的回调函数参数集合
    this._args = args;
    // 实例化的hook名称
    this.name = name;
    // 用来存放tap的集合,hook实例调用一次tap就往taps里面推送一条数据
    this.taps = [];
    // 拦截器集合
    this.interceptors = [];
    // 三种调用方法,_开头的是私有成员,仅供类内部使用
    this._call = CALL_DELEGATE;
    this.call = CALL_DELEGATE;
    this._callAsync = CALL_ASYNC_DELEGATE;
    this.callAsync = CALL_ASYNC_DELEGATE;
    this._promise = PROMISE_DELEGATE;
    this.promise = PROMISE_DELEGATE;
    // _x就是注册的回调函数集合
    this._x = undefined;

    // 代码编译的函数
    this.compile = this.compile;

    // 创建异步钩子的三种方法
    // tap可以创建同步钩子和异步钩子,tap创建的钩子只能使用call调用
    this.tap = this.tap;
    this.tapAsync = this.tapAsync;
    this.tapPromise = this.tapPromise;
  }

  /**
   * 编译函数,被子类重写,也就是上面SyncHook中的COMPILE方法
   * @param {} options
   */
  compile(options) {
    throw new Error("Abstract: should be overridden");
  }

  /**
   * 创建call函数
   * @param {调用类型} type call callAsync promise
   * @returns 编译后的可执行函数
   */
  _createCall(type) {
    // 内部调用重写的编译方法,也就是调用代码生成的工厂函数
    // 最终返回一段可执行的代码,执行注册的所有回调
    return this.compile({
      taps: this.taps,
      interceptors: this.interceptors,
      args: this._args,
      type: type,
    });
  }

  /**
   * 注册钩子函数
   * @param {类型} type
   * @param {参数选项} options
   * @param {待注册的回调函数} fn
   * hook.tap('fn1', () => {}) --> fn1 就是 options  () => {} 就是 fn
   */
  // type Tap = TapOptions & {
  //  name: string;
  // };

  // type TapOptions = {
  //  before?: string;
  //  stage?: number;
  // };
  _tap(type, options, fn) {
    if (typeof options === "string") {
      // 如果这个参数选项是一个字符串,将其去除空格后作为钩子命名
      options = {
        name: options.trim(),
      };
    } else if (typeof options !== "object" || options === null) {
      // 如果这个参数选项不是一个对象,或者是null,那么就抛出错误信息
      throw new Error("Invalid tap options");
    }
    if (typeof options.name !== "string" || options.name === "") {
      // 如果参数选项的name不是一个字符串类型,或者是一个空字符串,那么就抛出错误信息
      throw new Error("Missing name for tap");
    }
    // 如果参数选项存在 content,那么就提示该方法已近弃用
    if (typeof options.context !== "undefined") {
      deprecateContext();
    }
    // 合并参数选项:{ type, fn, name }
    options = Object.assign({ type, fn }, options);
    // 参数选项经过拦截器处理
    options = this._runRegisterInterceptors(options);
    // 推入taps
    this._insert(options);
  }

  // 三个注册钩子函数的方法
  tap(options, fn) {
    this._tap("sync", options, fn);
  }

  tapAsync(options, fn) {
    this._tap("async", options, fn);
  }

  tapPromise(options, fn) {
    this._tap("promise", options, fn);
  }

  /**
   * 注册拦截器
   * @param {参数选项} options
   * @returns 经过拦截器处理后的参数选项
   */
  _runRegisterInterceptors(options) {
    // 遍历拦截器
    for (const interceptor of this.interceptors) {
      // 如果这个拦截器有注册方法,就去处理options
      if (interceptor.register) {
        const newOptions = interceptor.register(options);
        // 如果register返回的新options不为空,替换原options
        if (newOptions !== undefined) {
          options = newOptions;
        }
      }
    }
    // 最终返回经过处理的options
    return options;
  }

  /**
   * 组合参数选项
   * @param {参数选项} options
   * @returns
   */
  withOptions(options) {
    const mergeOptions = (opt) =>
      Object.assign({}, options, typeof opt === "string" ? { name: opt } : opt);

    return {
      name: this.name,
      tap: (opt, fn) => this.tap(mergeOptions(opt), fn),
      tapAsync: (opt, fn) => this.tapAsync(mergeOptions(opt), fn),
      tapPromise: (opt, fn) => this.tapPromise(mergeOptions(opt), fn),
      intercept: (interceptor) => this.intercept(interceptor),
      isUsed: () => this.isUsed(),
      withOptions: (opt) => this.withOptions(mergeOptions(opt)),
    };
  }

  /**
   * 是否被使用过了,如果taps或者interceptors不为空,说明已经注册了钩子函数
   * @returns
   */
  isUsed() {
    return this.taps.length > 0 || this.interceptors.length > 0;
  }

  /**
   * 为钩子函数注册拦截器
   * @param {拦截器} interceptor
   */
  intercept(interceptor) {
    // 重置编译器
    this._resetCompilation();
    // 添加拦截器
    this.interceptors.push(Object.assign({}, interceptor));
    // 如果这个拦截器有注册方法,那么就去处理注册的钩子函数
    if (interceptor.register) {
      for (let i = 0; i < this.taps.length; i++) {
        this.taps[i] = interceptor.register(this.taps[i]);
      }
    }
  }

  /**
   * 重置编译器,为下一次编译做准备
   */
  _resetCompilation() {
    this.call = this._call;
    this.callAsync = this._callAsync;
    this.promise = this._promise;
  }

  /**
   * 向taps中推入钩子函数
   * @param {注册的钩子函数} item --> 如果未经过拦截器处理,那么就是 { type, name, fn }
   */
  _insert(item) {
    // 重置一下编译器
    this._resetCompilation();
    // 调用tap创建钩子函数时第一个参数,也就数options可以是一个字符串,也可以是一个对象
    // 如果是字符串,就会将其去除首尾空格,作为钩子的命名
    // 如果是一个对象,可以配置一些参数,其中就包括before和stage
    // before表示在某个任务之前,类型限定为字符串
    let before;
    if (typeof item.before === "string") {
      // Set(1) {fn2}
      //   ▼ size (get):ƒ size()
      //      :1
      //   ▼ [[Entries]]:Array(1)
      //     ▶ 0:"fn2"
      //      length:1
      //     ▶ __proto__:Object
      //   ▶ __proto__:Set
      before = new Set([item.before]);
    } else if (Array.isArray(item.before)) {
      before = new Set(item.before);
    }
    // stage表示执行阶段,值越大越靠后执行
    // 限定为number类型
    let stage = 0;
    if (typeof item.stage === "number") {
      stage = item.stage;
    }
    let i = this.taps.length;
    while (i > 0) {
      // i自减1
      i--;
      // 取出taps中的第i项
      const x = this.taps[i];
      // taps中插入一项,插入的是原taps中的第i项,也就是将第i项下沉一位
      this.taps[i + 1] = x;
      // 初始化xStage表示当前tap的stage,不存在就默认为0,优先执行的
      const xStage = x.stage || 0;
      // 如果存在before,就会去找before对应的那个钩子名称
      if (before) {
        // 如果找到了,就删除掉before,然后跳出本轮循环,进入下次循环
        // 进入循环后会继续自减i,将找到的那个tap上面的一项拷贝一份下沉
        // 此时before还是存在的,只是size为0,会继续向下执行代码
        // 到i++,就是i自增1,此时的i就对应那个拷贝的一项,它此时正好在before对应tap上面
        // break就会跳出循环,最终this.taps[i] = item;就会将那个拷贝的一项替换成带有before的tap
        // 如果before对应的那项正好就是第0项,那么就直接跳出循环(循环条件i > 0不成立了),直接进行最终的赋值操作
        if (before.has(x.name)) {
          before.delete(x.name);
          continue;
        }
        // 如果没找到,就会跳出本轮循环,继续进入下轮循环,目的就是将before对应的那项之后的每一项都下沉一位,只到找到before对应的那个tap
        // 找到后就会执行上面的代码,进行替换准备
        if (before.size > 0) {
          continue;
        }
      }
      // 如果当前i项tap存在stage,且比item的stage还大,那么就跳出本轮循环
      // 继续执行下轮循环,将上轮的比item stage大的那项下沉
      // 直到当前i项的stage小于item的stage,说明当前i项的tap优先item执行
      // 条件不成立,继续执行代码,i++后跳出循环
      // 最终执行this.taps[i] = item;将下沉的那一项替换为item
      if (xStage > stage) {
        continue;
      }
      i++;
      break;
    }
    // 当taps为空时,不会进入循环体,将item直接推入taps
    this.taps[i] = item;
  }
}

Object.setPrototypeOf(Hook.prototype, null);

module.exports = Hook;

3. 执行钩子

hook.call('jack', 12)

SyncHookHook 的源码都在上面两步,主要就是创建 hook 实例,调用实例的 tap 方法注册钩子函数。最终 call 的执行顺序是这样的:

  1. 调用 hook 实例的 call 方法,指向 CALL_DELEGATE
  2. CALL_DELEGATE 第一句 this.call = this._createCall("sync"); 调用 Hook 的私有成员方法 __createCall,它又会调用自己的 compilecompile 会被子类重写,也就是 SyncHook 中的 COMPILE
  3. COMPILE 第一句 factory.setup(this, options); 调用 factory实例的 setup 方法挂载钩子回调函数至 hook 实例的 _x 属性上 instance._x = options.taps.map(t => t.fn);
  4. COMPILE 第二句 return factory.create(options); 就是调用 factory实例的 create 方法生成执行钩子回调的函数,并将生成的函数返回赋值给 hookcall 方法
  5. 最终 CALL_DELEGATE 第二句 return this.call(...args); 执行生成的执行函数,参数就是调用 call 时传入的值

以下就是生成的执行代码,存在于调用栈中(内存中):

/**
*  执行上下文就是 hook 实例,this._x 能够访问到 hook 实例的 _x 属性,也就是钩子回调集合
*  参数就是...args
*/
(function anonymous(name, age) {
  "use strict";
  var _context;
  var _x = this._x;
  var _fn0 = _x[0];
  _fn0(name, age);
  var _fn1 = _x[1];
  _fn1(name, age);
  var _fn2 = _x[2];
  _fn2(name, age);
});

HookCodeFactory 源码:

// HookCodeFactory.js

/*
 MIT License http://www.opensource.org/licenses/mit-license.php
 Author Tobias Koppers @sokra
*/
"use strict";

/**
 * 生成执行钩子回调函数的程式
 * 返回的是一个可执行函数,它的执行上下文是hook实例
 * 可执行函数最终在 Hook.js CALL_DELEGATE 第二句执行
 */
class HookCodeFactory {
  constructor(config) {
    this.config = config;
    this.options = undefined;
    this._args = undefined;
  }

  /**
   * 创建执行函数
   * @param { 参数选项 } options
   * {
   *  taps: this.taps,
   *  interceptors: this.interceptors,
   *  args: this._args,
   *  type: type
   * }
   * @returns function 代码片段
   */
  create(options) {
    // 初始化配置选项
    this.init(options);
    // 声明一个函数
    let fn;
    // 根据不同的type生成不同的执行函数,这里是同步钩子,只看sync部分
    switch (this.options.type) {
      case "sync":
        fn = new Function(
          // 第一个参数是函数形参(通过args方法组装而成)
          this.args(),
          // 第二个参数是函数体
          '"use strict";\n' +
            // 函数头部内容,主要是取出钩子回调函数
            this.header() +
            // 拦截器处理
            this.contentWithInterceptors({
              onError: (err) => `throw ${err};\n`,
              onResult: (result) => `return ${result};\n`,
              resultReturns: true,
              onDone: () => "",
              rethrowIfPossible: true,
            })
        );
        break;

      ...

    }
    this.deinit();
    return fn;
  }

  /**
   * 绑定钩子回调函数
   * @param {上下文hook} instance
   * @param {参数选项} options
   */
  setup(instance, options) {
    instance._x = options.taps.map((t) => t.fn);
  }

  /**
   * 初始化配置选项,缓存配置选项和调用参数
   * @param {{ type: "sync" | "promise" | "async", taps: Array<Tap>, interceptors: Array<Interceptor> }} options
   */
  init(options) {
    this.options = options;
    this._args = options.args.slice();
  }

  /**
   * 重置配置选项
   */
  deinit() {
    this.options = undefined;
    this._args = undefined;
  }

  contentWithInterceptors(options) {
    if (this.options.interceptors.length > 0) {
      // 拦截器处理后生成code
      ...
    } else {
      // 我们暂时不看拦截器,只生成代码,这里调用子类 syncHook 的 content 方法
      // 内部调用父类 hoolCodeFactory 的 callTapsSeries 方法
      // return this.callTapsSeries({
      //  onError: (i, err) => onError(err),
      //  onDone,
      //  rethrowIfPossible
      // });
      return this.content(options);
    }
  }

  /**
   * 拼装函数头部内容
   * @returns 函数头部内容
   */
  header() {
    let code = "";
    // hook.context 已经废弃了
    if (this.needContext()) {
      code += "var _context = {};\n";
    } else {
      code += "var _context;\n";
    }
    // 取出钩子回调函数
    code += "var _x = this._x;\n";
    // 取出拦截器
    if (this.options.interceptors.length > 0) {
      code += "var _taps = this.taps;\n";
      code += "var _interceptors = this.interceptors;\n";
    }
    return code;
  }

  needContext() {
    for (const tap of this.options.taps) if (tap.context) return true;
    return false;
  }

  callTap(tapIndex, { onError, onResult, onDone, rethrowIfPossible }) {
    let code = "";
    let hasTapCached = false;
    for (let i = 0; i < this.options.interceptors.length; i++) {
      const interceptor = this.options.interceptors[i];
      if (interceptor.tap) {
        if (!hasTapCached) {
          code += `var _tap${tapIndex} = ${this.getTap(tapIndex)};\n`;
          hasTapCached = true;
        }
        code += `${this.getInterceptor(i)}.tap(${
          interceptor.context ? "_context, " : ""
        }_tap${tapIndex});\n`;
      }
    }
    code += `var _fn${tapIndex} = ${this.getTapFn(tapIndex)};\n`;
    const tap = this.options.taps[tapIndex];
    switch (tap.type) {
      case "sync":
        if (!rethrowIfPossible) {
          code += `var _hasError${tapIndex} = false;\n`;
          code += "try {\n";
        }
        if (onResult) {
          code += `var _result${tapIndex} = _fn${tapIndex}(${this.args({
            before: tap.context ? "_context" : undefined,
          })});\n`;
        } else {
          code += `_fn${tapIndex}(${this.args({
            before: tap.context ? "_context" : undefined,
          })});\n`;
        }
        if (!rethrowIfPossible) {
          code += "} catch(_err) {\n";
          code += `_hasError${tapIndex} = true;\n`;
          code += onError("_err");
          code += "}\n";
          code += `if(!_hasError${tapIndex}) {\n`;
        }
        if (onResult) {
          code += onResult(`_result${tapIndex}`);
        }
        if (onDone) {
          code += onDone();
        }
        if (!rethrowIfPossible) {
          code += "}\n";
        }
        break;

      ...
    }
    return code;
  }

  /**
   * 同步调用钩子回调函数
   * @param {
   *  onError: 执行失败回调
   *  onResult: 执行结果回调
   *  resultReturns: 返回的执行结果
   *  onDone: 执行完毕回调
   *  doneReturns: 执行完毕后的返回结果
   *  rethrowIfPossible: 异常捕获
   * } param0
   * @returns
   */
  callTapsSeries({
    onError,
    onResult,
    resultReturns,
    onDone,
    doneReturns,
    rethrowIfPossible,
  }) {
    if (this.options.taps.length === 0) return onDone();
    const firstAsync = this.options.taps.findIndex((t) => t.type !== "sync");
    const somethingReturns = resultReturns || doneReturns;
    let code = "";
    let current = onDone;
    let unrollCounter = 0;
    for (let j = this.options.taps.length - 1; j >= 0; j--) {
      const i = j;
      const unroll =
        current !== onDone &&
        (this.options.taps[i].type !== "sync" || unrollCounter++ > 20);
      if (unroll) {
        unrollCounter = 0;
        code += `function _next${i}() {\n`;
        code += current();
        code += `}\n`;
        current = () => `${somethingReturns ? "return " : ""}_next${i}();\n`;
      }
      const done = current;
      const doneBreak = (skipDone) => {
        if (skipDone) return "";
        return onDone();
      };
      const content = this.callTap(i, {
        onError: (error) => onError(i, error, done, doneBreak),
        onResult:
          onResult &&
          ((result) => {
            return onResult(i, result, done, doneBreak);
          }),
        onDone: !onResult && done,
        rethrowIfPossible:
          rethrowIfPossible && (firstAsync < 0 || i < firstAsync),
      });
      current = () => content;
    }
    code += current();
    return code;
  }

  /**
   * 组装参数
   * @param {前置参数, 后置参数} param0
   * @returns 拼装好的回调参数
   */
  args({ before, after } = {}) {
    let allArgs = this._args;
    if (before) allArgs = [before].concat(allArgs);
    if (after) allArgs = allArgs.concat(after);
    if (allArgs.length === 0) {
      return "";
    } else {
      return allArgs.join(", ");
    }
  }

  getTapFn(idx) {
    return `_x[${idx}]`;
  }

  getTap(idx) {
    return `_taps[${idx}]`;
  }

  getInterceptor(idx) {
    return `_interceptors[${idx}]`;
  }
}

module.exports = HookCodeFactory;

以上代码删除了异步相关的代码片段。