美文网首页构建工具:Webpack
Webpack(二十五):webpack4核心模块tapable

Webpack(二十五):webpack4核心模块tapable

作者: CodeMT | 来源:发表于2020-10-15 14:44 被阅读0次

    webpack打包是一种事件流的机制,它的原理是将各个插件串联起来,那么实现这一切的核心就是我们要讲解的tapable. 并且在webpack中负责编译的Compiler和负责创建bundles的Compilation都是tapable构造函数的实列。

    WebPack的loader(加载器)和plugin(插件)是由Webpack开发者和社区开发者共同贡献的。如果我们想写加载器和插件的话,我们需要看懂Webpack的基本原理,也就是说要看懂Webpack源码,我们今天要讲解的 tapable 则是webpack依赖的核心库。因此在看懂webpack之前,我们先要把 tapable 这个代码看懂。

    我这边讲解的是基于 "webpack": "^4.16.1",这个版本的包的。安装该webpack之后,该webpack会自带 tapable 包。在tapable包下它是由如下js文件组成的。

    |---- tapable
    |  |--- AsyncParallelBailHook.js 
    |  |--- AsyncParallelHook.js
    |  |--- AsyncSeriesBailHook.js
    |  |--- AsyncSeriesHook.js
    |  |--- AsyncSeriesLoopHook.js
    |  |--- AsyncSeriesWaterfallHook.js
    |  |--- Hook.js
    |  |--- HookCodeFactory.js
    |  |--- HookMap.js
    |  |--- index.js
    |  |--- MultiHook.js
    |  |--- simpleAsyncCases.js
    |  |--- SyncBailHook.js
    |  |--- SyncHook.js
    |  |--- SyncLoopHook.js
    |  |--- SyncWaterfallHook.js
    |  |--- Tapable.js
    

    如下图所示:

    Tapable的本质是能控制一系列注册事件之间的执行流的机制。如上图我们可以看到,都是以Sync, Async 及 Hook结尾的方法,他们为我们提供了不同的事件流执行机制,我们可以把它叫做 "钩子"。那么这些钩子可以分为2个类别,即 "同步" 和 "异步", 异步又分为两个类别,"并行" 还是 "串行",同步的钩子它只有 "串行"。

    如下图所示:

    下面我会把所有的测试demo放在我们的项目结构下的 public/js/main.js 文件代码内,我们可以执行即可看到效果。下面是我们项目中的目录基本结构(很简单,无非就是一个很简单的运行本地demo的框架):

    github中demo框架

    可以把该框架下载到本地来,下面的demo可以使用该框架来测试代码了。

    |--- tapable项目
    | |--- node_modules  
    | |--- public
    | | |--- js
    | | | |--- main.js
    | |--- package.json
    | |--- webpack.config.js
    

    一:理解Sync类型的钩子

    1. SyncHook.js

    SyncHook.js 是处理串行同步执行的文件,在触发事件之后,会按照事件注册的先后顺序执行所有的事件处理函数。

    如下代码所示:

    const { SyncHook } = require('tapable');
    
    // 创建实列
    const syncHook = new SyncHook(["name", "age"]);
    
    // 注册事件
    syncHook.tap("1", (name, age) => {
      console.log("1", name, age);
    });
    syncHook.tap("2", (name, age) => {
      console.log("2", name, age);
    });
    syncHook.tap("3", (name, age) => {
      console.log("3", name, age);
    });
    
    // 触发事件,让监听函数执行
    syncHook.call("kongzhiEvent-1", 18);
    

    执行的结果如下所示:

    如上demo实列可以看到,在我们的tapable中,SyncHook是tapable中的一个类,首先我们需要创建一个实列,注册事件之前需要创建实列,创建实列时需要传入一个数组,该数组的存储的事件是我们在注册事件时,需要传入的参数。实列中的tap方法用于注册事件,该方法支持传入2个参数,第一个参数是 '事件名称', 第二个参数为事件处理函数,函数参数为执行call(触发事件)时传入的参数的形参。

    2. SyncBailHook.js

    SyncBailHook.js同样为串行同步执行,如果事件处理函数执行时有一个返回值不为空。则跳过剩下未执行的事件处理函数。

    如下代码所示:

    const { SyncBailHook } = require('tapable');
    
    // 创建实列
    
    const syncBailHook = new SyncBailHook(["name", "age"]);
    
    // 注册事件
    syncBailHook.tap("1", (name, age) => {
      console.log("1", name, age);
    });
    
    syncBailHook.tap("2", (name, age) => {
      console.log("2", name, age);
      return '2';
    });
    
    syncBailHook.tap("3", (name, age) => {
      console.log("3", name, age);
    });
    
    // 触发事件,让监听函数执行
    syncBailHook.call("kongzhiEvent-1", 18);
    

    如下图所示:

    如上代码我们可以看到,第一个注册事件,直接执行打印 console.log(); 打印信息出来,它会继续执行第二个事件,第二个注册事件有 return '2'; 有返回值,且返回值不为undefined,因此它会跳过后面的注册事件。因此如上就打印2条信息了。也就是说 syncBailHook 作用也是同步执行的,只是说如果我们的注册事件的回调函数有返回值,且返回值不为undefined的话,那么它就会跳过后面的注册事件。即立刻停止执行后面的监听函数。

    3. SyncWaterfallHook.js

    SyncWaterfallHook 为串行同步执行,上一个事件处理函数的返回值作为参数传递给下一个事件处理函数,依次类推。

    如下测试代码:

    const { SyncWaterfallHook } = require('tapable');
    
    // 创建实列
    const syncWaterfallHook = new SyncWaterfallHook(["name", "age"]);
    
    // 注册事件
    syncWaterfallHook.tap("1", (name, age) => {
      console.log("第一个函数事件名称", name, age);
      return '1';
    });
    
    syncWaterfallHook.tap("2", (data) => {
      console.log("第二个函数事件名称", data);
      return '2';
    });
    
    syncWaterfallHook.tap("3", (data) => {
      console.log("第三个函数事件名称", data);
      return '3';
    });
    
    // 触发事件,让监听函数执行
    const res = syncWaterfallHook.call("kongzhiEvent-1", 18);
    
    console.log(res);
    

    打印信息如下所示:

    4. SyncLoopHook.js

    SyncLoopHook 为串行同步执行,事件处理函数返回true表示继续循环,如果返回undefined的话,表示结束循环。

    如下代码演示:

    const { SyncLoopHook } = require('tapable');
    
    // 创建实列
    const syncLoopHook = new SyncLoopHook(["name", "age"]);
    
    // 定义辅助变量
    let total1 = 0;
    let total2 = 0;
    
    // 注册事件
    syncLoopHook.tap("1", (name, age) => {
      console.log("1", name, age, total1);
      return total1++ < 2 ? true : undefined;
    });
    
    syncLoopHook.tap("2", (name, age) => {
      console.log("2", name, age, total2);
      return total2++ < 2 ? true : undefined;
    });
    
    syncLoopHook.tap("3", (name, age) => {
      console.log("3", name, age);
    });
    
    // 触发事件,让监听函数执行
    syncLoopHook.call("kongzhiEvent-1", 18);
    

    执行的结果如下所示:

    执行结果如上所示,我们来理解下 SyncLoopHook 执行顺序,首先我们知道,SyncLoopHook 的基本原理是:事件处理函数返回true表示继续循环,如果返回undefined的话,表示结束循环。

    首先我们出发第一个注册事件函数,total1 依次循环,所以会打印 0, 1, 2 的值,因此打印的值如下所示:

    1 kongzhiEvent-1 18 0
    1 kongzhiEvent-1 18 1
    1 kongzhiEvent-1 18 2
    

    当 total1 = 2 的时候,再去判断 2 < 2 呢?所以最后值返回 undefined, 因此就执行第二个回调函数,但是此时由于 total1++; 因此 total1 = 3了。所以执行完成第二个 函数的时候,会打印信息如下:

    2 kongzhiEvent-1 18 0
    

    但是由于等于true,所以会继续循环函数,因此又会从第一个函数内部训话,因此第一个函数就会打印如下信息:

    1 kongzhiEvent-1 18 3
    

    但是由于 total1 = 3了,因此又返回undefined了,因此又会执行 第二个函数,这个时候 total2 = 1了,因此会打印:

    2 kongzhiEvent-1 18 1
    

    然后返回true,继续从第一个函数循环执行,此时的 total1 = 4; 因为在上次 total1=3 虽然条件不满足,但是还是会自增1的,因此会继续循环打印如下信息:

    1 kongzhiEvent-1 18 4
    

    此时 又不满足,因此会执行第二个函数,此时的 total2=2了,因为在上一次执行完成后,total2会自增1. 因此先打印如下信息:

    2 kongzhiEvent-1 18 2
    

    由于此时 total2 = 2; 因此最后返回undefined,因此会执行第三个函数,但是此时的 total2 = 3了,因为执行了 total2++; 所以最后一个函数会打印如下信息:

    3 kongzhiEvent-1 18
    

    如上就是 SyncLoopHook.js 函数的作用。

    二:理解Async类型的钩子

    Async类型可以使用tap, tapSync 和 tapPromise 注册不同类型的插件钩子,我们分别可以通过 call, callAsync, promise 方法调用。

    1. AsyncParallelHook

    AsyncParallelHook 为异步并行执行,如果是通过 tapAsync 注册的事件,那么我们需要通过callAsync触发,如果我们通过tapPromise注册的事件,那么我们需要promise触发。

    1)tapAsync/callAsync

    如下代码所示:

    const { AsyncParallelHook } = require('tapable');
    
    // 创建实列
    const asyncParallelHook = new AsyncParallelHook(["name", "age"]);
    
    // 注册事件
    asyncParallelHook.tapAsync("1", (name, age, done) => {
      setTimeout(() => {
        console.log("1", name, age, new Date());
        done();
      }, 1000);
    });
    
    asyncParallelHook.tapAsync("2", (name, age, done) => {
      setTimeout(() => {
        console.log("2", name, age, new Date());
        done();
      }, 2000);
    });
    
    asyncParallelHook.tapAsync("3", (name, age, done) => {
      setTimeout(() => {
        console.log("3", name, age, new Date());
        done();
      }, 3000);
    });
    
    // 触发事件,让监听函数执行
    asyncParallelHook.callAsync("kongzhiEvent-1", 18, () => {
      console.log('函数执行完毕');
    });
    

    执行结果如下所示:

    如上我们可以看到,该三个函数,第一个函数是 2019 17:10:55,第二个函数是 2019 17:10:56,第三个函数是 2019 17:10:57 执行完成了,上面三个函数定时器操作最长的时间也是3秒,我们把这三个函数执行完成总共也是使用了3秒的时间,说明了该三个事件处理函数是异步执行的了。不需要等待上一个函数结束后再执行下一个函数。

    tapAsync注册的事件函数最后一个参数为回调函数done,每个事件处理函数在异步代码执行完成后都会调用该done函数。因此就能保证我们的 callAsync会在所有异步函数执行完毕后就执行该回调函数。

    2)tapPromise/promise

    使用tapPromise注册的事件,必须返回一个Promise实列,promise方法也会返回一个Promise实列。

    如下代码演示:

    const { AsyncParallelHook } = require('tapable');
    
    // 创建实列
    const asyncParallelHook = new AsyncParallelHook(["name", "age"]);
    
    // 注册事件
    asyncParallelHook.tapPromise("1", (name, age) => {
      return new Promise((resolve, reject) => {
        setTimeout(() => {
          console.log("1", name, age, new Date());
        }, 1000);
      });
    });
    
    asyncParallelHook.tapPromise("2", (name, age) => {
      return new Promise((resolve, reject) => {
        setTimeout(() => {
          console.log("2", name, age, new Date());
        }, 2000);
      });
    });
    
    asyncParallelHook.tapPromise("3", (name, age) => {
      return new Promise((resolve, reject) => {
        setTimeout(() => {
          console.log("3", name, age, new Date());
        }, 3000);
      });
    });
    
    // 触发事件,让监听函数执行
    asyncParallelHook.promise("kongzhiEvent-1", 18);
    

    效果如下所示:

    如上代码所示,每个tabPromise注册事件的处理函数都会返回一个Promise实列,新版的代码可能改掉了,我们不能再promise函数内部使用 resolve('1') 这样的,在外部不能使用 asyncParallelHook.promise("kongzhiEvent-1", 18).then() 这样的,不支持then,刚看了源码,也没有返回一个新的promise对象。

    如上也可以看到,我们的第一个函数需要1秒后执行,第二个函数需要2秒后执行,第三个函数需要三秒后执行,但是我们打印的信息可以看到,总共花费了3秒时间,也就是说我们上面的三个函数也是并行执行的。并不是需要等前一个函数执行完毕后再执行后面的函数。

    2. AsyncSeriesHook

    AsyncSeriesHook 为异步串行执行的。和我们上面的 AsyncParallelHook一样,通过使用 tapAsync注册事件,通过callAsync触发事件,也可以通过 tapPromise注册事件,使用promise来触发。

    1)tapAsync/callAsync

    和我们上面的 AsyncParallelHook一样, AsyncParallelHook 的 callAysnc方法也是通过传入回调函数的方式,在所有事件函数处理完成后,我们需要执行 callAsync的回调函数。

    如下代码演示:

    const { AsyncSeriesHook } = require('tapable');
    
    // 创建实列
    const asyncSeriesHook = new AsyncSeriesHook(["name", "age"]);
    
    // 注册事件
    asyncSeriesHook.tapAsync("1", (name, age, done) => {
      setTimeout(() => {
        console.log("1", name, age, new Date());
        done();
      }, 1000);
    });
    
    asyncSeriesHook.tapAsync("2", (name, age, done) => {
      setTimeout(() => {
        console.log("2", name, age, new Date());
        done();
      }, 2000);
    });
    
    asyncSeriesHook.tapAsync("3", (name, age, done) => {
      setTimeout(() => {
        console.log("3", name, age, new Date());
        done();
      }, 3000);
    });
    
    // 触发事件,让监听函数执行
    asyncSeriesHook.callAsync("kongzhiEvent-1", 18, () => {
      console.log('执行完成');
    });
    

    执行结果如下所示:

    从上面打印信息我们可以看到,我们第一次打印 1 kongzhiEvent-1 18 Mon Aug 05 2019 17:58:09 GMT+0800 (中国标准时间) 这个信息,然后当我们执行第二个函数的时候,是2000毫秒后执行,因此打印第二条信息如下所示:

    2 kongzhiEvent-1 18 Mon Aug 05 2019 17:58:11 GMT+0800 (中国标准时间)
    

    接着我们执行第三个函数,隔了3000毫秒后执行,因此可以看到打印信息如下:

    3 kongzhiEvent-1 18 Mon Aug 05 2019 17:58:14 GMT+0800 (中国标准时间)
    

    因此我们可以看到该方法是串行执行的。

    2)tapPromise/promise

    和上面的 AsyncParallelHook 一样,使用tapPromise来注册事件函数,然后需要返回一个Promise实列,然后我们使用 promise 来触发该事件。

    如下代码所示:

    const { AsyncSeriesHook } = require('tapable');
    
    // 创建实列
    const asyncSeriesHook = new AsyncSeriesHook(["name", "age"]);
    
    // 注册事件
    asyncSeriesHook.tapPromise("1", (name, age) => {
      return new Promise((resolve, reject) => {
        setTimeout(() => {
          console.log("1", name, age, new Date());
          resolve();
        }, 1000);
      })
    });
    
    asyncSeriesHook.tapPromise("2", (name, age) => {
      return new Promise((resolve, reject) => {
        setTimeout(() => {
          console.log("2", name, age, new Date());
          resolve();
        }, 2000);
      });
    });
    
    asyncSeriesHook.tapPromise("3", (name, age) => {
      return new Promise((resolve, reject) => {
        setTimeout(() => {
          console.log("3", name, age, new Date());
          resolve();
        }, 3000);
      });
    });
    
    // 触发事件,让监听函数执行
    asyncSeriesHook.promise("kongzhiEvent-1", 18);
    

    如上代码执行效果如下所示:

    如上代码,我们对比 AsyncParallelHook 代码可以看到,唯一不同的是 该asyncSeriesHook的Promsie内部需要调用 resolve() 函数才会执行到下一个函数,否则的话,只会执行第一个函数,但是 AsyncParallelHook 不调用 resolve()方法会依次执行下面的函数。

    三:tapable源码分析

    先以SyncHook.js 源码分析:

    源码如下:

    /*
      MIT License http://www.opensource.org/licenses/mit-license.php
      Author Tobias Koppers @sokra
    */
    "use strict";
    
    const Hook = require("./Hook");
    const HookCodeFactory = require("./HookCodeFactory");
    
    class SyncHookCodeFactory extends HookCodeFactory {
      content({ onError, onResult, onDone, rethrowIfPossible }) {
        return this.callTapsSeries({
          onError: (i, err) => onError(err),
          onDone,
          rethrowIfPossible
        });
      }
    }
    
    const factory = new SyncHookCodeFactory();
    
    class SyncHook extends Hook {
      tapAsync() {
        throw new Error("tapAsync is not supported on a SyncHook");
      }
    
      tapPromise() {
        throw new Error("tapPromise is not supported on a SyncHook");
      }
    
      compile(options) {
        factory.setup(this, options);
        return factory.create(options);
      }
    }
    
    module.exports = SyncHook;
    

    如上代码我们可以看到 SyncHook 类它继承了 Hook 类,然后 定义了 SyncHookCodeFactory 类 继承了 HookCodeFactory 类,我们先来看看 Hook.js 相关的代码如下:

    class Hook {
      constructor(args) {
        // args 参数必须是一个数组,比如我们上面的demo 传递值为 ["name", "age"]
        if(!Array.isArray(args)) args = [];
    
        // 把数组args赋值给 _args的内部属性
        this._args = args;
    
        // 保存所有的tap事件
        this.taps = [];
    
        // 拦截器数组
        this.interceptors = [];
    
        // 调用 内部方法 _createCompileDelegate 然后把返回值赋值给内部属性 _call, 并且暴露给外部属性 call
        this.call = this._call = this._createCompileDelegate("call", "sync");
    
        /* 调用 内部方法 _createCompileDelegate ,然后把返回值赋值给内部属性 _promise,并且暴露外部属性 promise
        */
        this.promise = this._promise = this._createCompileDelegate("promise", "promise");
    
        /*
         调用 内部方法 _createCompileDelegate,然后把返回值赋值给内部属性 _callAsync, 并且暴露外部属性
         callAsync
        */
        this.callAsync = this._callAsync = this._createCompileDelegate("callAsync", "async");
    
        // 用于调用函数的时候,保存钩子数组的变量
        this._x = undefined;
      }
    }
    

    如上 类 Hook 是代码的初始化工作;对于上面的 _createCompileDelegate 这个方法,我们先不用管,该方法在我们的这个SyncHook 类暂时还用不上,因为这个是处理异步的操作,下面我们会讲解到的。下面我们需要看 注册事件 tap 方法.

    代码如下:

    tap(options, fn) {
      if(typeof options === "string")
        options = { name: options };
      if(typeof options !== "object" || options === null)
        throw new Error("Invalid arguments to tap(options: Object, fn: function)");
      options = Object.assign({ type: "sync", fn: fn }, options);
      if(typeof options.name !== "string" || options.name === "")
        throw new Error("Missing name for tap");
      // 注册拦截器
      options = this._runRegisterInterceptors(options);
      // 插入钩子
      this._insert(options);
    }
    

    如上该方法接收2个参数,第一个参数必须是一个字符串,如果它是字符串的话,那么 options = {name: options}, options 就返回了一个带name属性的对象了。然后使用 Object.assign()方法对对象合并,如下代码:options = Object.assign({ type: "sync", fn: fn }, options); 因此最后options就返回如下了:
    options = { type: "sync", fn: fn, name: options };

    然后就是调用如下方法,来注册一个拦截器;如下代码:

    options = this._runRegisterInterceptors(options);

    现在我们来看下 _runRegisterInterceptors 代码如下所示:

    _runRegisterInterceptors(options) {
      for(const interceptor of this.interceptors) {
        if(interceptor.register) {
          const newOptions = interceptor.register(options);
          if(newOptions !== undefined)
            options = newOptions;
        }
      }
      return options;
    }
    

    如上_runRegisterInterceptors() 方法是注册拦截器的方法,该方法有一个options参数,该options的值是:

    options = { type: "sync", fn: fn, name: '这是一个字符串' };
    

    在该函数内部我们遍历拦截器this.interceptors,然后拦截器 this.interceptors会有一个属性register,如果有该属性的话,就调用该属性来注册一个新的 newOptions 对象,如果 newOptions 对象 不等于 undefined的话,就把options = newOptions; 赋值给 options的 最后返回 options, 当然如果没有该拦截器的话,就直接返回该 options对象。

    如上代码我们知道,我们在调用 tap()方法来注册事件之前,我们就需要使用注册拦截器,来添加拦截器的,因为在调用_runRegisterInterceptors 方法时,它内部代码会遍历该拦截器,因此我们就可以判定在我们使用 tap 事件之前,我们就需要使用 添加拦截器. 下面我们来看看我们添加拦截器的代码如下:

    intercept(interceptor) {
      // 重置所有的调用方法
      this._resetCompilation();
      // 保存拦截器到全局属性 interceptors内部,我们使用 Object.assign方法复制了一份
      this.interceptors.push(Object.assign({}, interceptor));
      /*
       如果该拦截器有register属性的话,我们就遍历所有的taps, 把他们作为参数调用拦截器的register,并且把返回的tap对象
       (该tap对象指tap函数里面把fn和name这些信息组合起来的新对象)。然后赋值给 当前的某一项tap
      */
      if(interceptor.register) {
        for(let i = 0; i < this.taps.length; i++)
          this.taps[i] = interceptor.register(this.taps[i]);
      }
    }
    

    下面我们看下如下demo来继续理解下 intercept 方法的含义:如下demo所示:

    const { SyncHook } = require('tapable');
    
    const h1 = new SyncHook(['xxx']);
    
    h1.tap('A', function(args) {
      console.log('A', args);
      return 'b';
    });
    
    h1.tap('B', function() {
      console.log('b');
    });
    
    h1.tap('C', function() {
      console.log('c');
    });
    
    h1.tap('D', function() {
      console.log('d');
    });
    
    h1.intercept({
      call: (...args) => {
        console.log(...args, '11111111');
      },
      register: (tap) => {
        console.log(tap, '222222');
        return tap;
      },
      loop: (...args) => {
        console.log(...args, '33333');
      },
      tap: (tap) => {
        console.log(tap, '444444');
      }
    });
    

    运行效果如下所示:

    如上demo代码我们可以看到,我们在调用 tap 来注册我们的事件的时候,我们先会执行我们的拦截器,也就是调用我们的SyncHook类的实列对象 h1, 会调用 h1.intercept 方法的 register 函数,所以我们注册了多少次,就使用拦截器拦截了多少次,并且返回了一个新的对象, 比如返回了 {type: "sync", fn: fn, name: 'A'} 这样的新对象。

    我们可以在返回看下我们的 Hook.js 中的 tap(options, fn) {} 这个方法内部,该方法内部注册事件的时候,会先调用

    options = this._runRegisterInterceptors(options); 
    

    这个函数代码,该函数代码的作用是注册拦截器,然后返回新的对象回来,如下代码所示:

    _runRegisterInterceptors(options) {
      for(const interceptor of this.interceptors) {
        if(interceptor.register) {
          const newOptions = interceptor.register(options);
          if(newOptions !== undefined)
            options = newOptions;
        }
      }
      return options;
    }
    

    如上我们可以看到,它会返回了一个新对象,就是我们上面打印出来的对象。该值会保存到我们的 options 参数中,接着我们继续执行 taps函数中的最后一句代码:

    this._insert(options);

    该函数的代码,就是把所有的事件对象保存到 this.taps 中。保存完成后,那么 this.taps 就有该值了,然后这个时候我们就会调用我们上面的 intercept 中的 register 这个函数。 下面我们继续来看下 _insert(options) 中的代码吧,代码如下所示:

    _insert(item) {
      // 重置资源,因为每一个插件都会有一个新的 Compilation
      this._resetCompilation();
      // 该item.before 是插件的名称
      let before;
    
      // 打印item
      console.log(item);
    
      /*
       before 可以是单个字符串插件的名称,也可以是一个字符串数组的插件
       new Set 是ES6新增的,它的作用是去掉数组里面重复的值
      */
      if(typeof item.before === "string")
        before = new Set([item.before]);
      else if(Array.isArray(item.before)) {
        before = new Set(item.before);
      }
    
      let stage = 0;
      if(typeof item.stage === "number")
        stage = item.stage;
      let i = this.taps.length;
      while(i > 0) {
        console.log('----', i);
        i--;
        const x = this.taps[i];
        this.taps[i+1] = x;
        const xStage = x.stage || 0;
        if(before) {
          if(before.has(x.name)) {
            before.delete(x.name);
            continue;
          }
          if(before.size > 0) {
            continue;
          }
        }
        if(xStage > stage) {
          continue;
        }
        i++;
        break;
      }
      // 打印i的值
      console.log(i);
      this.taps[i] = item;
    }
    

    如上代码使用while循环,遍历所有的taps的函数,然后会根据stage和before进行重新排序,stage的优先级低于before。如下demo,我们也可以如下调用 tap, 比如tap是一个数组。如下所示:

    const { SyncHook } = require('tapable');
    
    const h1 = new SyncHook(['xxx']);
    
    h1.tap('A', function(args) {
      console.log('A', args);
      return 'b';
    });
    
    h1.tap('B', function() {
      console.log('b');
    });
    
    h1.tap('C', function() {
      console.log('c');
    });
    h1.tap({
      name: 'F',
      before: 'D'
    }, function() {
      
    });
    h1.tap({
      name: 'E',
      before: 'C'
    }, function() {
    
    });
    h1.tap('D', function() {
      console.log('d');
    });
    
    h1.intercept({
      call: (...args) => {
        console.log(...args, '11111111');
      },
      register: (tap) => {
        console.log(tap, '222222');
        return tap;
      },
      loop: (...args) => {
        console.log(...args, '33333');
      },
      tap: (tap) => {
        console.log(tap, '444444');
      }
    });
    

    打印后的效果如下所示:

    如上我们可以看到我们的 _insert(item); 方法中打印的console.log(item);项的值及打印console.log(i)的值及console.log('----', i); 我们可以看下如上的 _insert(item)方法中的算法如下理解:

    1. 首先在我们的代码demo里面使用 tap 注册一个A事件,h1.tap('A', function(args) {}), 因此最后会把该函数返回的对象传递进来,对象为 {type: 'sync', fn: fn, name: 'A'}; 因此打印的 console.log(item); 就是该对象值,初始化第一步i的值为0. 因为 this.taps.length 的长度为0. 所以第一次不会进入 while循环内部,执行到最后我们就把 console.log(i) 的值打印出来。

    2. 当我们使用 tap注册一个B事件的时候, h1.tap('B', function(args) {}); console.log(item); 就会打印出对象的值为:
    {type: 'sync', fn:fn, name: 'B'}; 然后判断是否有before或state这个属性,如果没有的话,直接跳过,while (i > 0); 进入该while循环内部,打印 console.log('----', i); 因此会打印 '---- 1' 这样的,i--, 执行完后 i 的值会减一. const x = this.taps[i]; 因此 x = this.taps[0] = {type: 'sync', fn: fn, name: 'A'}, this.taps[i+1] = x; 因此 this.taps[1] = {type: 'sync', fn: fn, name: 'A'}; 这样的。const xStage = x.stage || 0; 因此 xStage = 0; 因为我们没有stage这个属性,也没有before属性,所以也不会进入上面的if语句,最后我们会进行如下判断:

    if(xStage > stage) {
       continue;
    }
    

    也不会进入if语句,然后 i++; 因此此时 i = 1 了;因此会打印1. 此时我们的 this.taps = [{type: 'sync', fn: fn, name: 'A'}, {type: 'sync', fn: fn, name: 'B'}];

    3. 当我们使用tap注册一个C事件的时候,h1.tap('C', function() {}); 同理,打印出我们的 console.log(item) 的值变为如下:
    {type: "sync", fn: ƒ, name: "C"},一样也没有before和state属性,如果没有,直接跳过,接着就打印 ---2 然后此时 i=2了,最后我们的

    this.taps = [{type: 'sync', fn: fn, name: 'A'}, {type: 'sync', fn: fn, name: 'B'}, {type: "sync", fn: ƒ, name: "C"}];
    

    4. 当我们注册F事件的时候,如代码:h1.tap({name: 'F', before: 'D'}, function() {}), 此时 i = this.taps.length = 3; 因此第一次会打印 '---- 3';
    const x = this.taps[i]; const x = this.taps[2] = {type: "sync", fn: ƒ, name: "C"}; this.taps[i+1] = x; 因此 this.taps[3] = {type: "sync", fn: ƒ, name: "C"}; 然后会判断是否有before或state这个属性,我们注册F事件的时候可以看到,它有before这个属性了,因此在内部i的值会从3依次循环,直接0为止,每次循环内部自己减少1. 因此最后我们的i的值就变为0了,因此我们的 this.tabs值就变成这样的了:

    this.tabs = [
      {type: 'sync', name: 'F', before:'D', fn: fn},   
      {type: 'sync', fn: fn, name: 'A'}, 
      {type: 'sync', fn: fn, name: 'B'}, 
      {type: "sync", fn: ƒ, name: "C"}
    ];
    

    5. 当我们注册事件E的时候,如代码 h1.tap({name: 'E', before: 'C'}, function(){}); 此时我们的i = 4了,因此会打印 '---- 4',
    const x = this.taps[i]; const x = this.taps[3] = {type: "sync", fn: ƒ, name: "C"}; this.taps[i+1] = this.taps[4] = {type: "sync", fn: ƒ, name: "C"}; 然后会判断是否有before或state这个属性,我们注册E事件的时候可以看到,它有before这个属性了,
    后面逻辑依次类推....

    上面代码分析的优点烦,我们再来整理下思路,理解下 上面的算法:

    1. 假如我们注册了如下代码:

    h1.tap('A', function(args) {}); 
    h1.tap('B', function(args) {});
    h1.tap('C', function(args) {});
    h1.tap({name: 'F', before: 'D'}, function(args) {}); 
    h1.tap({ name: 'E', before: 'C'}, function() {}); 
    h1.tap('D', function() { console.log('d'); });
    

    如上注册了这么多函数,也就是说,我们每次注册一个函数都会传递一个对象进来,

    比如类似这样的:

    {type: 'sync', fn: fn, name: 'A'}, 
    {type: 'sync', fn: fn, name: 'B'}, 
    {type: 'sync', fn: fn, name: 'C'}, 
    {type: 'sync', fn: fn, name: 'F', before: 'D'}, 
    {type: 'sync', fn: fn, name: 'E', before: 'C'}, 
    {type: 'sync', fn: fn, name: 'D'}.
    

    会依次调用我们的 _insert(item) 这个函数,item的值就是上面我们的依次循环的每个对象的值。

    2. 第一次传递 {type: 'sync', fn: fn, name: 'A'} 这个对象进来后,由于第一次我们的 let i = this.taps.length; 的长度为0;因此就不会进行 代码的while内部循环,因此我们的 this.taps = [{type: 'sync', fn: fn, name: 'A'}]; 这样的值。

    3. 第二次传递 {type: 'sync', fn: fn, name: 'B'} 这个对象进来的时候,这次我们的 let i = this.taps.length; 的长度为1了,因此
    就会进入while循环,const x = this.taps[i]; const x = {type: 'sync', fn: fn, name: 'A'}; this.taps[i+1] = {type: 'sync', fn: fn, name: 'A'}; 因此这个时候 我们的 this.taps = [{type: 'sync', fn: fn, name: 'A'},{type: 'sync', fn: fn, name: 'A'}] 了。由于该对象事件没有 stage 或 before 这个参数,因此最后执行 i++; 因此i的值变为1,最后一句代码:this.taps[i] = item; item的值就是我们的 这个对象 {type: 'sync', fn: fn, name: 'B'}; 因此此时的 this.taps 的值,变为如下:

    this.taps = [{type: 'sync', fn: fn, name: 'A'}, {type: 'sync', fn: fn, name: 'B'}];
    

    4. 第三次传递 {type: 'sync', fn: fn, name: 'C'} 这个对象进来的时候,和我们的第三步骤一样,依次类推,因此最后我们的 this.taps 的值变为如下:

    this.taps = [{type: 'sync', fn: fn, name: 'A'}, {type: 'sync', fn: fn, name: 'B'}, {type: 'sync', fn: fn, name: 'C'}];
    

    5. 第四次传递 {type: 'sync', fn: fn, name: 'F', before: 'D'} 这个对象进来的时候,此时我们的 let i = this.taps.length = 3 了; 因此就会进入while循环,执行如下代码:i--; const x = this.taps[i]; this.taps[i+1] = x; 因此

    const x = this.taps[2]
    = {type: 'sync', fn: fn, name: 'C'}; this.taps[i+1] = this.taps[3] = {type: 'sync', fn: fn, name: 'C'};
    

    因此此时我们的 this.taps 对象的值变为如下:

    this.taps = [
    {type: 'sync', fn: fn, name: 'A'}, 
    {type: 'sync', fn: fn, name: 'B'}, 
    {type: 'sync', fn: fn, name: 'C'}, 
    {type: 'sync', fn: fn, name: 'C'}
    ];
    

    最后我们就会执行下面的代码:

    if(before) {
      if(before.has(x.name)) {
        before.delete(x.name);
        continue;
      }
      if(before.size > 0) {
        continue;
      }
    }
    

    因为事件F有before这个参数,因此会进入if条件判断语句了,接着就判断 before.has(x.name); 判断该对象是否有 before 该值,比如我们上面的的before为字符串 'D', 判断我们之前保存的 this.taps 数组内部的每项对象的name属性是否有 'D' 这个字符串。如果有的话,就直接删除该对象。 所以一直没有找到 'D' 字符,因此会一直判断 if(before.size > 0) { continue; } 进行对内部i循环,因此i = 3;就循环了3次,依次是3, 2, 1 这样的,最后 i-- ;i = 0的时候,就不会进入while循环内部了,因此我们在第一个位置会插入 F事件了,比如:

    this.taps[0] = {type: 'sync', fn: fn, name: 'F', before: 'D'};
    

    注意:上面再内部依次循环 3, 2, 1 的时候,我们的this.taps的值数组会发生改变的,比如等于3的时候,我们的数组是如下这个样子,因为执行了如下代码:

    i--;
    const x = this.taps[i];
    this.taps[i+1] = x;
    

    i = 3 时,this.taps的值为 = [{type: 'sync', fn: fn, name: 'A'}, {type: 'sync', fn: fn, name: 'B'}, {type: 'sync', fn: fn, name: 'C'}, {type: 'sync', fn: fn, name: 'C'}];

    i = 2 时,this.taps的值为 = [{type: 'sync', fn: fn, name: 'A'}, {type: 'sync', fn: fn, name: 'B'}, {type: 'sync', fn: fn, name: 'B'}, {type: 'sync', fn: fn, name: 'C'}];

    i = 1时,this.taps的值为 = [{type: 'sync', fn: fn, name: 'A'}, {type: 'sync', fn: fn, name: 'A'}, {type: 'sync', fn: fn, name: 'B'}, {type: 'sync', fn: fn, name: 'C'}];

    最后也就是我们上面的 i = 0的时候,因此我们会把 {type: 'sync', fn: fn, name: 'F', before: 'D'}; 对象值插入到我们的 taps数组的第一个位置上了,因此 this.taps的值最终变为:[{type: 'sync', fn: fn, name: 'F', before: 'D'}, {type: 'sync', fn: fn, name: 'A'}, {type: 'sync', fn: fn, name: 'B'}, {type: 'sync', fn: fn, name: 'C'}];

    6. 第五次传递 {type: 'sync', fn: fn, name: 'E', before: 'C'} 的时候,也是同样的道理,此时我们的 let i = this.taps.length = 4 了; 因此就会进入while循环,执行如下代码:i--; const x = this.taps[i]; this.taps[i+1] = x; 因此我们的 const x = this.taps[3] = {type: 'sync', fn: fn, name: 'C'}; this.taps[i+1] = this.taps[4] = {type: 'sync', fn: fn, name: 'C'};

    在while内部循环,同样的道理也会执行如下代码:

    if(before) {
      if(before.has(x.name)) {
        before.delete(x.name);
        continue;
      }
      if(before.size > 0) {
        continue;
      }
    }
    if(xStage > stage) {
      continue;
    }
    i++;
    break;
    

    首先是有before这个参数的,因此会进入if语句内部,然后判断该before是否有该 x.name 属性吗?before的属性值为 'C'; 因此判断该有没有x.name 呢,我们从上面知道我们的 this.taps 的值为 =

    [
    {type: 'sync', fn: fn, name: 'F', before: 'D'}, 
    {type: 'sync', fn: fn, name: 'A'}, 
    {type: 'sync', fn: fn, name: 'B'}, 
    {type: 'sync', fn: fn, name: 'C'}, 
    {type: 'sync', fn: fn, name: 'C'}
    ];
    

    从上面 i-- 可知,我们此时i的值为3,因此我们需要把该值插入到 this.taps[3] = {type: 'sync', fn: fn, name: 'E', before: 'C'} 了; 因此此时 this.taps的值就变为如下了;

    this.taps = [
    {type: 'sync', fn: fn, name: 'F', before: 'D'}, 
    {type: 'sync', fn: fn, name: 'A'}, 
    {type: 'sync', fn: fn, name: 'B'}, 
    {type: 'sync', fn: fn, name: 'E', before: 'C'}, 
    {type: 'sync', fn: fn, name: 'C'}
    ];
    

    7. 第六次传递的D事件,也是一个意思,这里就不再分析了,因此我们的 this.taps的值最终变为如下:

    this.taps = [
    {type: 'sync', fn: fn, name: 'F', before: 'D'}, 
    {type: 'sync', fn: fn, name: 'A'}, 
    {type: 'sync', fn: fn, name: 'B'}, 
    {type: 'sync', fn: fn, name: 'E', before: 'C'}, 
    {type: 'sync', fn: fn, name: 'C'}, 
    {type: 'sync', fn: fn, name: 'D'}
    ];
    

    如上就是一个排序算法; 大家可以理解下。至于stage属性也是一样的,只是before属性的优先级相对于stage会更高。

    理解 _createCompileDelegate() 函数代码

    我们再回到我们的 Hook.js 中的构造函数内部有如下几句代码:

    class Hook {
      constructor(args) {
        this.call = this._call = this._createCompileDelegate("call", "sync");
        this.promise = this._promise = this._createCompileDelegate("promise", "promise");
        this.callAsync = this._callAsync = this._createCompileDelegate("callAsync", "async");
      }
    }
    

    如上代码,我们可以看到我们的 this.call, this.promise, this.callAsync 都会调用 内部函数 _createCompileDelegate, 我们来看看该内部函数的代码如下所示:

    _createCompileDelegate(name, type) {
      const lazyCompileHook = (...args) => {
        this[name] = this._createCall(type);
        return this[name](...args);
      };
      return lazyCompileHook;
    }
    

    如上可以看到,_createCompileDelegate 函数接收2个参数,name 和 type,就是我们上面调用该函数的时候传递进来的。然后在内部使用闭包的形式返回了 lazyCompileHook 函数,因此 this.call, this.promise, this.callAsync 都返回了该函数 lazyCompileHook 。

    我们再来看下如上demo,加上如下测试代码如下所示:

    const { SyncHook } = require('tapable');
    
    const h1 = new SyncHook(['xxx']);
    
    h1.tap('A', function(args) {
      console.log('A', args);
      return 'b';
    });
    
    h1.tap('B', function() {
      console.log('b');
    });
    
    h1.tap('C', function() {
      console.log('c');
    });
    h1.tap({
      name: 'F',
      before: 'D'
    }, function() {
      
    });
    h1.tap({
      name: 'E',
      before: 'C'
    }, function() {
    
    });
    h1.tap('D', function() {
      console.log('d');
    });
    h1.call(7777);
    

    如上我们打印的结果如下所示:

    如上我们调用call方法后,会因此执行 如上面的注册事件的回调函数,我们再来看下_createCompileDelegate函数内部代码

    _createCompileDelegate(name, type) {
      const lazyCompileHook = (...args) => {
        this[name] = this._createCall(type);
        return this[name](...args);
      };
      return lazyCompileHook;
    }
    

    该函数内部代码,返回了lazyCompileHook函数给我们的call对象,然后当我们的 this.call(7777)的时候就会调用lazyCompileHook函数,传递了一个参数,因此 ...args = 7777; 内部代码:

    this[name] = this._createCall(type); 也就是说 这边的this对象指向了 SyncHook 的实列了,也就是我们外面的实列 h1对象了,this['call'] = this._createCall('sync'); 我们下面看下 _createCall 函数代码如下:

    _createCall(type) {
      return this.compile({
        taps: this.taps,
        interceptors: this.interceptors,
        args: this._args,
        type: type
      });
    }
    
    compile(options) {
      throw new Error("Abstract: should be overriden");
    }
    

    如上代码,我们是不是萌了?compile方法直接抛出一个对象?当然不是,我们在 SyncHook 这个类中(其他的类也是一样),会对该方法进行重写的,我们可以看下我们的 SyncHook中的类代码,如下所示:

    const HookCodeFactory = require("./HookCodeFactory");
    const factory = new SyncHookCodeFactory();
    
    class SyncHook extends Hook {
      tapAsync() {
        throw new Error("tapAsync is not supported on a SyncHook");
      }
    
      tapPromise() {
        throw new Error("tapPromise is not supported on a SyncHook");
      }
    
      compile(options) {
        factory.setup(this, options);
        return factory.create(options);
      }
    }
    

    如上我们可以看到 我们的 compile 方法会进行重写该方法。如上的compile方法中的options的参数值就是我们上面传递进来的,如下所示

    options = {
      taps: this.taps,
      interceptors: this.interceptors,
      args: this._args,
      type: type
    }
    

    如上看到,我们引用了 HookCodeFactory 类进来,并且使用了 该类的实列 factory 中的 setUp()方法及 create()方法,我们看下该 HookCodeFactory 类代码如下所示:

    class HookCodeFactory {
      constructor(config) {
        this.config = config;
        this.options = undefined;
      }
      setup(instance, options) {
        instance._x = options.taps.map(t => t.fn);
      }
      create(options) {
    
      }
    }
    

    如上 setup 中的参数 instance 就是调用该实列对象了,options的参数值就是我们在 SyncHook.js 的参数如下值:

    options =  {
      taps: this.taps,
      interceptors: this.interceptors,
      args: this._args,
      type: type
    }
    

    其中 this.taps 值就是我们的上面的那个数组。 比如 :

    this.taps = [
    {type: 'sync', fn: fn, name: 'F', before: 'D'}, 
    {type: 'sync', fn: fn, name: 'A'}, 
    {type: 'sync', fn: fn, name: 'B'}, 
    {type: 'sync', fn: fn, name: 'E', before: 'C'}, 
    {type: 'sync', fn: fn, name: 'C'}, 
    {type: 'sync', fn: fn, name: 'D'}
    ];
    

    这样的, 然后每个事件对象的实列都绑定到 instance._x = fn. 这里面的fn就是我们this.taps数组里面遍历的fn函数。
    每个注册事件对应一个函数。会把该对应的事件函数绑定到 instance._x 上面来。我们接下来再看下 我们的 create()函数。

    create()函数代码如下所示:

    create(options) {
      this.init(options);
      switch(this.options.type) {
        case "sync":
          return new Function(this.args(), "\"use strict\";\n" + this.header() + this.content({
            onError: err => `throw ${err};\n`,
            onResult: result => `return ${result};\n`,
            onDone: () => "",
            rethrowIfPossible: true
          }));
        case "async":
          return new Function(this.args({
            after: "_callback"
          }), "\"use strict\";\n" + this.header() + this.content({
            onError: err => `_callback(${err});\n`,
            onResult: result => `_callback(null, ${result});\n`,
            onDone: () => "_callback();\n"
          }));
        case "promise":
          let code = "";
          code += "\"use strict\";\n";
          code += "return new Promise((_resolve, _reject) => {\n";
          code += "var _sync = true;\n";
          code += this.header();
          code += this.content({
            onError: err => {
              let code = "";
              code += "if(_sync)\n";
              code += `_resolve(Promise.resolve().then(() => { throw ${err}; }));\n`;
              code += "else\n";
              code += `_reject(${err});\n`;
              return code;
            },
            onResult: result => `_resolve(${result});\n`,
            onDone: () => "_resolve();\n"
          });
          code += "_sync = false;\n";
          code += "});\n";
          return new Function(this.args(), code);
      }
    }
    
    /**
     * @param {{ type: "sync" | "promise" | "async", taps: Array<Tap>, interceptors: Array<Interceptor> }} options
     */
    init(options) {
      this.options = options;
      this._args = options.args.slice();
    }
    

    如上代码,其中我们的 create(options) 函数中的参数 options 的值为如下:

    options = {
      taps: this.taps,
      interceptors: this.interceptors,
      args: this._args,
      type: type
    }
    

    而我们的type值为 'sync', 因此会进入case语句中的第一个case,该create函数内部,判断三种类型的情况,分别为 'sync', 'async', 'promise'.

    如上代码 options 对象中的参数:taps 是我们的注册事件对象的数组,interceptors 是过滤器,目前是 []; 我们的demo里面没有使用过滤器,当然我们也可以使用过滤器,args 参数值为 ['xxx']; 我们初始化实列的时候 传递了该值;比如如下初始化该类代码:const h1 = new SyncHook(['xxx']); 然后我们的type为 'sync' 了 。因为我们 h1实列调用的是call这个方法。搞清楚了上面各个参数的含义,我们接下来往下看。

    在create()方法内部,我们首先会调用 init() 方法,如下代码所示:this.init(options); 在init内部代码中,

    init(options) {
      this.options = options;
      this._args = options.args.slice();
    }
    

    this.options = options, 保存了该对象的引用。this._args = options.args.slice(); 保存了该数组传递进来的参数。

    现在就会直接 case 'sync' 的情况了,如下代码所示:

    case "sync":
    return new Function(this.args(), "\"use strict\";\n" + this.header() + this.content({
      onError: err => `throw ${err};\n`,
      onResult: result => `return ${result};\n`,
      onDone: () => "",
      rethrowIfPossible: true
    }));
    

    就会依次调用 this.args(); this.header(); this.content() 方法; 在 new Function(); 中如何调用方法看如下代码来理解,如下图所示:


    下面我们来看下 header() 方法如下代码所示:

    header() {
      let code = "";
      // this.needContext() 判断数组this.taps的某一项是否有 context属性,任意一项有的话,就返回true
      if(this.needContext()) {
        // 如果为true的话,var _context = {};
        code += "var _context = {};\n";
      } else {
        // 否则的话, var _context; 值为undefined
        code += "var _context;\n";
      }
      /*
       在setup()中,我们把所有的tap对象都给到了 instance, 因此这里的 this._x 就是我们之前说的 instance._x;
      */
      code += "var _x = this._x;\n";
      // 如果有拦截器的话,保存拦截器数组到局部变量 _interceptors 中,且数组保存到 _taps中。
      if(this.options.interceptors.length > 0) {
        code += "var _taps = this.taps;\n";
        code += "var _interceptors = this.interceptors;\n";
      }
      /*
       如果有拦截器的话,遍历。
       获取到某一个拦截器 const interceptor = this.options.interceptors[i];
       如果该拦截器有call这个方法的话,就拼接字符串。因此如果有过滤器的话,最终会拼接成如下字符串:
    
       "use strict";
        function(options) {
          var _context;
          var _x = this._x;
          var _taps = this.taps;
          var _interceptors = this.interceptors;
          // 下面就是循环拦截器,如果有一个拦截器的话
          _interceptors[0].call(options);
        }
      */
      for(let i = 0; i < this.options.interceptors.length; i++) {
        const interceptor = this.options.interceptors[i];
        if(interceptor.call) {
          code += `${this.getInterceptor(i)}.call(${this.args({
            before: interceptor.context ? "_context" : undefined
          })});\n`;
        }
      }
      return code;
    }
    
    needContext() {
      for(const tap of this.options.taps)
        if(tap.context) return true;
      return false;
    }
    getInterceptor(idx) {
      return `_interceptors[${idx}]`;
    }
    

    首先我们来看下 needContext() 函数,该函数遍历 this.options.taps;它是我们传进来的对象。this.options.taps 值如下:

    this.options.taps = [
    {type: 'sync', fn: fn, name: 'F', before: 'D'}, 
    {type: 'sync', fn: fn, name: 'A'}, 
    {type: 'sync', fn: fn, name: 'B'}, 
    {type: 'sync', fn: fn, name: 'E', before: 'C'},
     {type: 'sync', fn: fn, name: 'C'}, 
    {type: 'sync', fn: fn, name: 'D'}
    ];
    

    如果该数组中的某一个对象有 context 属性的话(该数组中任意一项),否则都没有context属性的话,会返回false。

    如上代码:

    if(this.needContext()) {
      code += "var _context = {};\n";
    } else {
      code += "var _context;\n";
    }
    code += "var _x = this._x;\n";
    

    如果 this.needContext() 为true的话,var _context = {}; 否则的话 var _context; 值为undefined; 因此如果this.needContext() 返回true的话,code的值变为如下所示:

    如果this.needContext()方法返回false的话,就返回如下所示的值:

    我们现在再来看看 this.content() 方法,content()方法并不在我们的HookCodeFactory类中,它是子类自己实现的,因此我们到 SyncHook类中去看代码如下所示:

    class SyncHookCodeFactory extends HookCodeFactory {
      content({ onError, onResult, onDone, rethrowIfPossible }) {
        return this.callTapsSeries({
          onError: (i, err) => onError(err),
          onDone,
          rethrowIfPossible
        });
      }
    }
    

    我们再结合 HookCodeFactory.js类中,看create()函数的代码:

    case "sync":
    return new Function(this.args(), "\"use strict\";\n" + this.header() + this.content({
      onError: err => `throw ${err};\n`,
      onResult: result => `return ${result};\n`,
      onDone: () => "",
      rethrowIfPossible: true
    }));
    

    如上代码,我们的content方法中传的参数为一个对象;

    {
      onError: err => `throw ${err};\n`,
      onResult: result => `return ${result};\n`,
      onDone: () => "",
      rethrowIfPossible: true
    }
    

    因此上面的 SyncHookCodeFactory 类 继承了 HookCodeFactory 中对应的参数为:

    onError =  err => `throw ${err};\n`; 
    onResult = result => `return ${result};\n`;
    onDone = () => "";
    rethrowIfPossible = true;
    

    如上 onError, onResult, onDone 都是一个函数,然后返回不同的值。最后我们调用 callTapsSeries 方法来执行; 下面我们来看下该 callTapsSeries 方法;方法在 HookCodeFactory 类中,代码如下所示:

    callTapsSeries({ onError, onResult, onDone, rethrowIfPossible }) {
      if(this.options.taps.length === 0)
        return onDone();
      const firstAsync = this.options.taps.findIndex(t => t.type !== "sync");
      const next = i => {
        if(i >= this.options.taps.length) {
          return onDone();
        }
        const done = () => next(i + 1);
        const doneBreak = (skipDone) => {
          if(skipDone) return "";
          return onDone();
        }
        return this.callTap(i, {
          onError: error => onError(i, error, done, doneBreak),
          onResult: onResult && ((result) => {
            return onResult(i, result, done, doneBreak);
          }),
          onDone: !onResult && (() => {
            return done();
          }),
          rethrowIfPossible: rethrowIfPossible && (firstAsync < 0 || i < firstAsync)
        });
      };
      return next(0);
    }
    

    如上代码:if(this.options.taps.length === 0) { return onDone(); } 的含义:如果 taps 处理完毕后或一个taps的长度都没有的话,就执行 onDone 方法,返回一个空字符串。

    const firstAsync = this.options.taps.findIndex(t => t.type !== "sync"); 如果第一个异步的下标index. 通过t.type !== 'sync' 来判断,如果没有异步的话,就返回 -1; findIndex的使用方式如下所示:


    下面我们来看这个函数调用,如下next方法如下所示:

    const next = i => {
      if(i >= this.options.taps.length) {
        return onDone();
      }
      const done = () => next(i + 1);
      const doneBreak = (skipDone) => {
        if(skipDone) return "";
        return onDone();
      }
      return this.callTap(i, {
        onError: error => onError(i, error, done, doneBreak),
        onResult: onResult && ((result) => {
          return onResult(i, result, done, doneBreak);
        }),
        onDone: !onResult && (() => {
          return done();
        }),
        rethrowIfPossible: rethrowIfPossible && (firstAsync < 0 || i < firstAsync)
      });
    };
    return next(0);
    

    默认情况下,我们看到 i = 0; 开始传递参数进去,如果 i 大于我们的 注册事件函数的 this.taps的数组的话,就直接返回 我们上面的 onDone()方法。如果不大于,就定义 done 函数,依次递归调用该next()函数,注意我们这边的 done 函数目前还没有被执行到。只是定义了一个 done函数方法放在这里,接下来就是我们的 doneBreak 函数了,它接收一个参数为 skipDone;如果有该参数的话,直接返回空字符串,否则的话,返回调用 onDone() 方法。最关键的一步在最后,最后我们返回了 this.callTap 这个函数,也就是说,我们的 callTapsSeries 函数方法的返回值决定于 callTap 这个方法的返回值。之前我们定义了 done() 函数递归调用及 定义了 doneBreak 函数都是为 callTap 函数做准备的。callTap函数接收2个参数,第一个参数为 i 值,第二个参数为一个对象,该对象定义了 onError,onResult 及 onDone,rethrowIfPossible 函数。

    callTap 函数变成如下:

    this.callTap(i, {
      onError: function(error) {
        onError(i, error, done, doneBreak);
      },
      onResult: onResult && function(result) {
        return onResult(i, result, done, doneBreak);
      },
      onDone: !onResult && function() {
        return done();
      },
      rethrowIfPossible: rethrowIfPossible && (firstAsync < 0 || i < firstAsync)
    });
    

    如上就是callTap函数的最终形式了,如果有onResult的话,就会返回一个匿名函数function, 然后我们调用该函数onResult即可。
    如果我们没有 onResult 函数话,那么我们可以调用 done函数,该done函数我们上面定义了如下代码:const done = () => next(i + 1); 因此如果我们没有传递onResult函数的话,它会依次循环我们之前使用 this.taps保存的所有事件,然后依次循环该事件 对应的回调函数。就好比我们下面的demo一样当我们调用 call 方法后,它会依次调用该回调函数,然后输出信息出来,如下demo:

    const { SyncHook } = require('tapable');
    
    const h1 = new SyncHook(['xxx']);
    
    h1.tap('A', function(args) {
      console.log('A', args);
      return 'b';
    });
    
    h1.tap('B', function() {
      console.log('b');
    });
    
    h1.tap('C', function() {
      console.log('c');
    });
    h1.tap({
      name: 'F',
      before: 'D'
    }, function() {
      
    });
    h1.tap({
      name: 'E',
      before: 'C'
    }, function() {
    
    });
    h1.tap('D', function() {
      console.log('d');
    });
    h1.call(7777);
    

    如上依次输出 A 7777 b c d

    我们再看看 rethrowIfPossible: rethrowIfPossible && (firstAsync < 0 || i < firstAsync) rethrowIfPossible 默认返回true,因此会执行后面的语句,如果我们的注册事件中有异步函数的话,那么我们的 firstAsync 参数就会返回该异步函数的索引值,因为我们上面的demo,注册事件没有异步函数,因此我们的 firstAsync 返回的值是 -1; 因此 -1 < 0; 因此返回true;后面的 i < firstAsync; 看不看无所谓,因为这里使用了 || 这个语句符。当然如果我们注册事件中有异步函数的话,那么我们就会继续 判断 i < firstAsync 这个语句了。如果rethrowIfPossible 是false的话,那么当前的钩子函数的类型就不是 sync,可能是Async或promise类型了。

    下面我们来看下 callTap 函数,代码如下所示:

    /*
      tapIndex 是下标索引。
      onError: onError(i, error, done, doneBreak);
      onResult:undefined, 因为上面调用的时候 没有 onResult 这个参数,所以返回undefined
      onDone: done(); 会递归调用我们上面的 next() 函数。
      rethrowIfPossible:默认为true.
       如果为false的话,说明当前的钩子不是 sync,如果为true的话,说明当前的钩子函数是 Async 或 Promise
     */
     callTap(tapIndex, { onError, onResult, onDone, rethrowIfPossible }) {
      let code = "";
      let hasTapCached = false;
      // 遍历拦截器,如果有拦截器的话,如果有就执行拦截器的tap函数
      for(let i = 0; i < this.options.interceptors.length; i++) {
        const interceptor = this.options.interceptors[i];
        if(interceptor.tap) {
          if(!hasTapCached) {
            /*
             如下代码,我们调用 this.getTap(tapIndex)方法后,会生成 `var _tap[0] = _tap[0]` 等这样的字符串。
             生成完成后,我们设置 hasTapCached 为true。如果有多个拦截器的话,我们也会执行一次。
             注意:我们这边获取 _taps 对象的下标是使用我们传进来的参数 tapIndex。在for循环中,我们的tapIndex值不会改变的
             。
            */
            code += `var _tap${tapIndex} = ${this.getTap(tapIndex)};\n`;
            hasTapCached = true;
          }
          /*
            下面的代码返回的是:code += `_interceptors[0].tap(_tap0)`;
            首先会判断该拦截器是否有 context 这个属性,如果有的话就获取 _context 这个属性,否则的话就空字符串。
          */
          code += `${this.getInterceptor(i)}.tap(${interceptor.context ? "_context, " : ""}_tap${tapIndex});\n`;
        }
      }
      /*
       下面的代码返回了:
       code += `var _fn0 = _x[0]`
      */
      code += `var _fn${tapIndex} = ${this.getTapFn(tapIndex)};\n`;
      // 获取 this.taps的索引,获取第一个或第n个
      const tap = this.options.taps[tapIndex];
      // 判断类型,是否是 sync, Async 及 Promise 对象的
      /*
       rethrowIfPossible 默认为true,同步执行,如果有异步的话,rethrowIfPossible 返回false,就执行if语句代码,
       因此代码 code += `var _hasError0 = false`; code += "try { \n" 这样的,如果是异步的话,因为要保证异步顺序的
       问题,因此这边使用了 try catch 这样的语句,防止报错发生。
      */
      switch(tap.type) {
        case "sync":
          if(!rethrowIfPossible) {
            code += `var _hasError${tapIndex} = false;\n`;
            code += "try {\n";
          }
          /*
           判断 onResult 是否为true还是false, 
           如果为true的话,那么 code += `var _result0 = _fn0(options)`
           如果为false的话,code += `_fn0(options)`; 这样的方法调用
          */
          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`;
          }
          // 把catch语句拼接上
          if(!rethrowIfPossible) {
            code += "} catch(_err) {\n";
            code += `_hasError${tapIndex} = true;\n`;
            code += onError("_err");
            code += "}\n";
            code += `if(!_hasError${tapIndex}) {\n`;
          }
          // 有 onResult 的话,code += onResult(`_result0`); 就调用该方法执行。这边是字符串拼接。
          if(onResult) {
            code += onResult(`_result${tapIndex}`);
          }
          // 如果有 onDone() 方法的话,就开始递归调用。我们之前有 next(i+1); 这样的递归。
          if(onDone) {
            code += onDone();
          }
          if(!rethrowIfPossible) {
            code += "}\n";
          }
          /*
           因此如果我们注册的是同步事件的话,那么我们的最终代码就变成如下:
           var _tap[0] = _tap[0];
           _interceptors[0].tap(_tap0);
           var _fn0 = _x[0];
           _fn0(options);
    
           如果我们的this.taps 有多个同步事件的话,会依次类推... 因此会有如下这样的:
           var _tap[0] = _tap[0];
           _interceptors[0].tap(_tap0);
           var _fn0 = _x[0];
           _fn0(options);
    
           var _tap[1] = _tap[1];
           _interceptors[1].tap(_tap1);
           var _fn1 = _x[1];
           _fn0(options);
           ..... 依次类推
          */
          break;
        case "async":
          let cbCode = "";
          if(onResult)
            cbCode += `(_err${tapIndex}, _result${tapIndex}) => {\n`;
          else
            cbCode += `_err${tapIndex} => {\n`;
          cbCode += `if(_err${tapIndex}) {\n`;
          cbCode += onError(`_err${tapIndex}`);
          cbCode += "} else {\n";
          if(onResult) {
            cbCode += onResult(`_result${tapIndex}`);
          }
          if(onDone) {
            cbCode += onDone();
          }
          cbCode += "}\n";
          cbCode += "}";
          code += `_fn${tapIndex}(${this.args({
            before: tap.context ? "_context" : undefined,
            after: cbCode
          })});\n`;
          break;
        case "promise":
          code += `var _hasResult${tapIndex} = false;\n`;
          code += `_fn${tapIndex}(${this.args({
            before: tap.context ? "_context" : undefined
          })}).then(_result${tapIndex} => {\n`;
          code += `_hasResult${tapIndex} = true;\n`;
          if(onResult) {
            code += onResult(`_result${tapIndex}`);
          }
          if(onDone) {
            code += onDone();
          }
          code += `}, _err${tapIndex} => {\n`;
          code += `if(_hasResult${tapIndex}) throw _err${tapIndex};\n`;
          code += onError(`_err${tapIndex}`);
          code += "});\n";
          break;
      }
      return code;
    }
    
    getTap(idx) {
      return `_taps[${idx}]`;
    }
    
    getInterceptor(idx) {
      return `_interceptors[${idx}]`;
    }
    
    getTapFn(idx) {
      return `_x[${idx}]`;
    }
    

    因此我们这边同步事件在 SyncHook 类中的 compile方法:

    compile(options) {
      factory.setup(this, options);
      return factory.create(options);
    }
    

    在返回代码之前,我们还是整理下整个思路吧,我们首先在 Hook类中代码如下:

    class Hook {
      _createCall(type) {
        return this.compile({
          taps: this.taps,
          interceptors: this.interceptors,
          args: this._args,
          type: type
        });
      }
    }
    

    在我们的子类 SyncHook中重写了 compile 该方法,代码如下:

    compile(options) {
      factory.setup(this, options);
      return factory.create(options);
    }
    

    因此我们会调用 setup该方法,代码如下:

    setup(instance, options) {
      instance._x = options.taps.map(t => t.fn);
    }
    

    最后我们会调用 factory.create(options); 这句代码,因此会调用 HookCodeFactory.js 代码中的 create()方法。
    该方法判断了三种类型,分别为 sync, Async, promise 等。因为我们这边都是同步事件,因此会调用 sync 这个case情况。
    因此我们最终代码返回变成如下:

    "use strict"
    function(options) {
      // 我们首先执行 HookCodeFactory类中的 header() 方法生成代码
      var _context;
      var _x = this._x;
    
      // 如果我们有拦截器的话,下面代码也会生成的,如果没有就忽略下面三句代码:
      var _taps = this.taps;
      var _interceptors = this.interceptors;
      /* 
        如果我们只有一个拦截器的话,只会生成一个,如果我们有多个的话,就会使用for循环生成多个
      */
      _interceptors[0].call(options); 
    
      // 下面就是我们的callTap函数返回的代码了
       var _tap[0] = _tap[0];
       _interceptors[0].tap(_tap0);
       var _fn0 = _x[0];
       _fn0(options);
    
       var _tap[1] = _tap[1];
       _interceptors[1].tap(_tap1);
       var _fn1 = _x[1];
       _fn0(options);
       ..... 依次类推
    }
    

    如上差不多就是一整个同步事件的流程了。至于其他的异步Async 和 promise,大家有空可以去折腾一下,里面的逻辑有点多。
    按照上面的我们思路也可以简单的折腾下了。我们可以看到,我们上面的同步事件的demo是如何被执行的,及它的回调函数是什么时候被执行的。当然里面还有很多参数判断,我们可以去根据API文档或他们写的测试用例去理解下应该差不多了。

    相关文章

      网友评论

        本文标题:Webpack(二十五):webpack4核心模块tapable

        本文链接:https://www.haomeiwen.com/subject/lqlvsktx.html