webpack4核心模組tapable源碼解析

  • 2019 年 10 月 3 日
  • 筆記

閱讀目錄

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 += "elsen";            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文檔或他們寫的測試用例去理解下應該差不多了。