tapable源码结构初识

1,307 阅读3分钟

本文是tapable项目源码阅读的第一篇,文章的目的是大致梳理一下tapable源码的结构,为接下来深入细节铺平道理,打好基础。

文章假设读者对tapable的用法有基本的了解,tapable文档在这里

整体感知

话不多说,直接上项目结构图:

-tapable
  |-lib
    |-index.js
    |-Hook.js
    |-SyncHook.js
    |-SyncBailHook.js 
    |-SyncLoopHook.js
    |-SyncWaterfallHook.js
    |-AsyncParallelBailHook.js
    |-AsyncParallelHook.js
    |-AsyncSeriesBailHook.js
    |-AsyncSeriesHook.js
    |-AsyncSeriesLoopHook.js
    |-AsyncSeriesWaterfallHook.js
    |-HookCodeFactory.js
    |-HookMap.js
    |-MultiHook.js
    |-util-browser.js
  |-package.json
  |-__tests__
    |- 测试用例

项目的结构非常简单,可以看到tapable支持的所有hook类型以及两个辅助类型都以单文件的形式存在。

通过package.jsonmain字段找到项目的入口index.js

// index.js
exports.SyncHook = require("./SyncHook");
exports.SyncBailHook = require("./SyncBailHook");
// ...

入口内容很简单,仅仅是将所有的引入并导出了tapable支持的10种hook类型,以及两个辅助类。

Hook的实例方法探究

可以提前剧透的一点是,tapable导出的10种hook类型,其实例化的流程是基本一致的,所以接下来,我们选定最简单的SyncHook类型来探究项目的大体结构。

看代码之前,我们思考一个问题:我们希望在SyncHook的代码里面看到什么?

根据SyncHook的用法,我想大概有以下几点:

  1. 插件注册的tap方法
  2. 执行所有已注册插件的call方法
  3. hook的拦截器注册intercept方法

带着期待,我们进入SyncHook.js

function SyncHook(args = [], name = undefined) {
  const hook = new Hook(args, name);
  hook.constructor = SyncHook;
  hook.tapAsync = TAP_ASYNC;
  hook.tapPromise = TAP_PROMISE;
  hook.compile = COMPILE;
  return hook;
}

SyncHook.prototype = null;

SyncHook子类利用实例继承的方式,继承了Hook父类,并将constructor指向自身,重写了tapAsynctapPromisecompile方法。对于SyncHook类型的钩子,tapAsynctapPromise方法调用都会直接抛出错误。compile方法,我们暂时还不知道有啥用。

我们期待中的tapcallintercept方法并没有出现,所以,接着去基类Hook.js文件去找。

Hook类结构如下:

class Hook {
  constructor(args = [], name = undefined) {
    // ...
    this.taps = [];
    this.interceptors = [];
    this.call = CALL_DELEGATE;
    this.callAsync = CALL_ASYNC_DELEGATE;
    this.promise = PROMISE_DELEGATE;

    this.compile = this.compile;
    this.tap = this.tap;
    this.tapAsync = this.tapAsync;
    this.tapPromise = this.tapPromise;
  }

  intercept() { /**/ }
  tap() { /**/ }
  tapAsync() { /**/ }
  tapPromise() { /**/ }
}

果然,我们期待的所有方法,都有在这里定义,我们逐一来看。

插件注册方法:tap

首先来看插件注册的tap方法(注意tap是SyncHook类的注册方法,对于其他类型的钩子,还有其他的注册方式)。

class Hook {
  // ...
  tap(options, fn) {
    this._tap("sync", options, fn);
  }
  _tap(type, options, fn) {
    // 省略标准化options的代码,标准化之后options包含了type和fn信息
    this._insert(options);
  }

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

  tapPromise(options, fn) {
    this._tap("promise", options, fn);
  }
  _insert(item) {
    // 省略寻找插入位置的逻辑代码
    this.taps[i] = item;
  }
}

从这里可以看出,对于一个hook实例,其插件注册的本质,其实是将我们传入的参数标准化之后,推入实例的taps数组中。并且,taps是一个有序的列表,可以推测这和插件将来执行的顺序是有关系的(排序的标准将在未来的文章中详细讲解)。

以上讲的是SyncHook的插件注册方式,那其他类型的插件呢?

在tapable中,插件的注册方式被分为了三大类(有的钩子类型支持多种注册方式),sync,async,promise,对应的插件注册方法分别是tap,tapAsync,tapPromise,这三者的注册方式完全一样,都是调用了this._tap将插件配置插入到taps数组中。

钩子拦截器方法:intercept

class Hook {
  // ...
  intercept(interceptor) {
    this._resetCompilation();
    this.interceptors.push(Object.assign({}, interceptor));
    if (interceptor.register) {
      // 中间件注册时,对所有**已经**注册的插件执行register函数
      for (let i = 0; i < this.taps.length; i++) {
        this.taps[i] = interceptor.register(this.taps[i]);
      }
    }
  }
}

插件注册的代码也很简单,其本质是将intercept方法接收到的参数直接推入实例属性interceptors数组中。同时遍历已经注册的插件,以插件的配置为参数,执行当前拦截器的register方法。(interceptor配置还有很多其他方法,比如,call, tap, loop等等,它们将在合适的时候执行)

插件列表的调用方法:call

首先注意,call是SyncHook类型的插件调用方法,还有其他的调用方式(callAsync,promise)

相对插件注册和拦截器注册,插件调用方法要复杂的多。

回到Hook.js

const CALL_DELEGATE = function(...args) {
  this.call = this._createCall("sync");
  return this.call(...args);
};

class Hook {
  constructor(args = [], name = undefined) {
    // ... 省略其他代码
    this.call = CALL_DELEGATE;
  }

  // 根据提供的类型,生成对应的插件执行方式
  _createCall(type) {
    return this.compile({
      // ...
    });
  }
  
  compile(options) {
    throw new Error("Abstract: should be overridden");
  }
}

call方法的执行被代理到CALL_DELEGATE函数,实际执行逻辑的call方法是由this._createCall生成,而该方法直接返回了this.compile方法的执行结果,所以可以总结为,插件调用的逻辑是由钩子实例的compile方法生成。而这里的compile方法是一个抽象方法,需要在具体的钩子里面去实现。

我们正在以SyncHook为例,所以,进入到SyncHook.js,找到compile方法:

const factory = new SyncHookCodeFactory();

const COMPILE = function(options) {
  factory.setup(this, options);
  return factory.create(options);
}

function SyncHook(args = [], name = undefined) {
  const hook = new Hook(args, name);
  // ...
  // compile方法生成调用hook插件的call/callAsync/promise方法
  hook.compile = COMPILE;
  return hook;
}
class SyncHookCodeFactory extends HookCodeFactory {
  content({ onError, onDone, rethrowIfPossible }) {
    return this.callTapsSeries({
      onError: (i, err) => onError(err),
      onDone,
      rethrowIfPossible
    });
  }
}

这里的compile方法也是由SyncHookCodeFactory实例的create方法生成,该类继承于HookCodeFactory,并定义了content方法。没找到create,继续去HookCodeFactory找:

class HookCodeFactory {
  // ...
  // 生成compile函数,compile函数返回的是hook的调用方法,比如synchook的call方法
  create(options) {
    // ...
    let fn;
    switch (this.options.type) {
      case "sync":
        fn = new Function(
          this.args(),
          '"use strict";\n' +
            this.header() + // 参数定义
            this.contentWithInterceptors({
              // ...
            })
        );
        break;
      case "async":
        // ...
      case "promise":
        // ...
    }
    // ...
    return fn;
  }

  setup(instance, options) {
    instance._x = options.taps.map(t => t.fn);
  }
  
  contentWithInterceptors(options) {
    if (this.options.interceptors.length > 0) {
      // ...
      let code = "";
      for (let i = 0; i < this.options.interceptors.length; i++) {
        const interceptor = this.options.interceptors[i];
        if (interceptor.call) {
          // 执行所有的中间件的call函数
          code += `${this.getInterceptor(i)}.call(${this.args({
            before: interceptor.context ? "_context" : undefined
          })});\n`;
        }
      }

      code += this.content(
        Object.assign(options, { /**/ })
      );
      return code;
    } else {
      return this.content(options);
    }
  }

前面说到,代码工厂类的create方法生成了钩子实例的compile方法(compile方法执行生成插件调用的方法call)。观察create方法代码,它根据传入钩子类型,利用Function构造函数实例化出一个函数并返回,这个返回的函数就是钩子实例的compile方法。

进一步看这个函数的实例化过程,主要的逻辑都在this.contentWithInterceptors里面,该方法先检查有没有拦截器,如果有拦截器,则在添加插件执行代码之前,先加上拦截器call方法的执行逻辑。插件逻辑代码则由this.content添加,此方法在特定类型的工厂类型中定义,对于SyncHook类钩子就是SyncHookCodeFactory工厂类。

有点绕,我们试着捋一捋:

SyncHook类型的钩子,在实例化之后,并没有调度插件逻辑的方法,虽然有一个call,但这是一个傀儡函数,执行这个函数时,才会调用compile编译出一个真正的调度函数call,然后执行。(啥?Hook.js的_createCall方法是干啥的?如果你试着删掉_createCall直接调用this.compile也行,不过三个代理函数是不是多了三处重复的代码?所以理解源码逻辑时,剥掉这一层会少绕一点弯子)

compile的本质是生成了调度插件的函数,而compile直接返回了create的执行结果,所以create方法的本质也是生成了调度插件的函数。

create方法定义在工厂类型的基类上,该方法通过Function构造函数,逐行生成插件调度方法的逻辑代码,其主要的调度逻辑代码由工厂类方法contentWithInterceptors生成

contentWithInterceptors则通过工厂实例方法content生成插件逻辑调度的代码,同时,它需要处理拦截器,在添加插件逻辑之前,它遍历了所有的拦截器,并添加了interceptor.call方法的执行逻辑。

把这个逻辑翻过来(其中factory是工厂类实例):

factory.content(生成插件调度主体代码的字符串) -> factory.contentWithInterceptors(在插件调度代码之前添加拦截器执行代码字符串) -> factory.create(添加函数变量定义并实例化函数) -> hook.compile(设置工厂配置,调用create生成并返回调度函数) -> hook.call

以上即是整个tapable项目的整体结构的初步感知,细节内容,敬请期待。