源碼學習記錄: tapable

前言

上一遍博文中,咱們談到了tapable的用法,如今咱們來深刻一下tap到底是怎麼運行的, 怎麼處理,控制 tap 進去的鉤子函數,攔截器又是怎麼運行的.前端

先從同步函數開始分析,異步也就是回調而已;webpack

tap

這裏有一個例子web

let SyncHook = require('./lib/SyncHook.js')

let h1 = new SyncHook(['options']);

h1.tap('A', function (arg) {
  console.log('A',arg);
  return 'b'; // 除非你在攔截器上的 register 上調用這個函數,否則這個返回值你拿不到.
})

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, '-------------intercept call');
  },
  //
  register: (tap) => {
  console.log(tap, '------------------intercept register');

    return tap;
  },
  loop: (...args) => {
    console.log(...args, '-------------intercept loop')
  },
  tap: (tap) => {
    console.log(tap, '-------------------intercept tap')

  }
})
h1.call(6);

new SyncHook(['synchook'])

首先先建立一個同步鉤子對象,那這一步會幹什麼呢?算法

這一步會先執行超類Hook的初始化工做後端

// 初始化
constructor(args) {
  // 參數必須是數組
  if (!Array.isArray(args)) args = [];
  // 把數組參數賦值給 _args 內部屬性, new 的時候傳進來的一系列參數.
  this._args = args;
  // 綁定taps,應該是事件
  this.taps = [];
  // 攔截器數組
  this.interceptors = [];
  // 暴露出去用於調用同步鉤子的函數
  this.call = this._call;
  // 暴露出去的用於調用異步promise函數
  this.promise = this._promise;
  // 暴露出去的用於調用異步鉤子函數
  this.callAsync = this._callAsync;
  // 用於生成調用函數的時候,保存鉤子數組的變量,如今暫時先無論.
  this._x = undefined;
}

第二部 .tap()

如今咱們來看看調用了tap() 方法後發生了什麼數組

tap(options, fn) {
  // 下面是一些參數的限制,第一個參數必須是字符串或者是帶name屬性的對象,
  // 用於標明鉤子,並把鉤子和名字都整合到 options 對象裏面
  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);
}
  • 如今咱們來看看如何註冊攔截器
_runRegisterInterceptors(options) {
  // 如今這個參數應該是這個樣子的{fn: function..., type: sync,name: 'A' }
// 遍歷攔截器,有就應用,沒有就把配置返還回去
for (const interceptor of this.interceptors) {
  if (interceptor.register) {
    // 把選項傳入攔截器註冊,從這裏能夠看出,攔截器的register 能夠返回一個新的options選項,而且替換掉原來的options選項,也就是說能夠在執行了一次register以後 改變你當初 tap 進去的方法
    const newOptions = interceptor.register(options);
    if (newOptions !== undefined) options = newOptions;
  }
}
return options;
}

注意: 這裏執行的register攔截器是有順序問題的, 這個執行在tap()裏面,也就是說,你這個攔截器要在調用tap(),以前就調用 intercept()添加的.promise

那攔截器是怎麼添加進去的呢,來看下intercept()緩存

intercept(interceptor) {
  // 重置全部的 調用 方法,在教程中咱們提到了 編譯出來的調用方法依賴的其中一點就是 攔截器. 全部每添加一個攔截器都要重置一次調用方法,在下一次編譯的時候,從新生成.
  this._resetCompilation();
  // 保存攔截器 並且是複製一份,保留本來的引用
  this.interceptors.push(Object.assign({}, interceptor));
  // 運行全部的攔截器的register函數而且把 taps[i],(tap對象) 傳進去.
  // 在intercept 的時候也會遍歷執行一次當前全部的taps,把他們做爲參數調用攔截器的register,而且把返回的 tap對象(tap對象就是指 tap函數裏面把fn和name這些信息整合起來的那個對象) 替換了原來的 tap對象,因此register最好返回一個tap, 在例子中我返回了原來的tap, 可是其實最好返回一個全新的tap
  if (interceptor.register) {
    for (let i = 0; i < this.taps.length; i++)
      this.taps[i] = interceptor.register(this.taps[i]);
  }
}

注意: 也就是在調用tap() 以後再傳入的攔截器,會在傳入的時候就爲每個tap 調用register方法閉包

  • 如今咱們來看看_insert
_insert(item) {
  // 重置資源,由於每個插件都會有一個新的Compilation
  this._resetCompilation();
  // 順序標記, 這裏聯合 __test__ 包裏的Hook.js一塊兒使用
  // 看源碼不懂,能夠看他的測試代碼,就知道他寫的是什麼目的.
  // 從測試代碼能夠看到,這個 {before}是插件的名字.
  let before;
  // before 能夠是單個字符串插件名稱,也能夠是一個字符串數組插件.
  if (typeof item.before === "string") {
    before = new Set([item.before]);
  }
  else if (Array.isArray(item.before)) {
    before = new Set(item.before);
  }
  // 階段
  // 從測試代碼能夠知道這個也是一個控制順序的屬性,值越小,執行得就越在前面
  // 並且優先級低於 before
  let stage = 0;
  if (typeof item.stage === "number") stage = item.stage;
  let i = this.taps.length;
  // 遍歷全部`tap`了的函數,而後根據 stage 和 before 進行從新排序.
  // 假設如今tap了 兩個鉤子  A B  `B` 的配置是  {name: 'B', before: 'A'}
  while (i > 0) {// i = 1, taps = [A]
    i--;// i = 0 首先-- 是由於要從最後一個開始
    const x = this.taps[i];// x = A
    this.taps[i + 1] = x;// i = 0, taps[1] = A  i+1 把當前元素日後移位,把位置讓出來
    const xStage = x.stage || 0;// xStage = 0
    if (before) {// 若是有這個屬性就會進入這個判斷
      if (before.has(x.name)) {// 若是before 有x.name 就會把這個插件名稱從before這個列表裏刪除,表明這個鉤子位置已經在當前的鉤子以前
        before.delete(x.name);
        continue;// 若是before還有元素,繼續循環,執行上面的操做
      }
      if (before.size > 0) {
        continue;// 若是before還有元素,那就一直循環,直到第一位.
      }
    }
    if (xStage > stage) {// 若是stage比當前鉤子的stage大,繼續往前挪
      continue;
    }
    i++;
    break;
  }
  this.taps[i] = item;// 把挪出來的位置插入傳進來的鉤子
}

這其實就是一個排序算法, 根據before, stage 的值來排序,也就是說你能夠這樣tap進來一個插件架構

h1.tap({
  name: 'B',
  before: 'A'
  }, () => {
    console.log('i am B')
  })

發佈訂閱模式

發佈訂閱模式是一個在先後端都盛行的一個模式,前端的promise,事件,等等都基於發佈訂閱模式,其實tapable 也是一種發佈訂閱模式,上面的tap 只是訂閱了鉤子函數,咱們還須要發佈他,接下來咱們談談h1.call(),跟緊了,這裏面纔是重點.

咱們能夠在初始化中看到this.call = this._call,那咱們來看一下 this._call() 是個啥

Object.defineProperties(Hook.prototype, {
  _call: {
    value: createCompileDelegate("call", "sync"),
    configurable: true,
    writable: true
  },
  _promise: {
    value: createCompileDelegate("promise", "promise"),
    configurable: true,
    writable: true
  },
  _callAsync: {
    value: createCompileDelegate("callAsync", "async"),
    configurable: true,
    writable: true
  }
});

結果很明顯,這個函數是由createCompileDelegate(),這個函數返回的,依賴於,函數的名字以及鉤子的類型.

createCompileDelegate(name, type)

function createCompileDelegate(name, type) {
  return function lazyCompileHook(...args) {
    // 子類調用時,this默認綁定到子類
    // (不明白的能夠了解js this指向,一個函數的this指向調用他的對象,沒有就是全局,除非使用call apply bind 等改變指向)
    // 在咱們的例子中,這個 this 是 SyncHook
    this[name] = this._createCall(type);
    // 用args 去調用Call
    return this[name](...args);
  };
}

在上面的註釋上能夠加到,他經過閉包保存了nametype的值,在咱們這個例子中,這裏就是this.call = this._createCall('sync');而後把咱們外部調用call(666) 時 傳入的參數給到他編譯生成的方法中.

注意,在咱們這個例子當中我在call的時候並無傳入參數.

這時候這個call方法的重點就在_createCall方法裏面了.

_createCall()

_createCall(type) {

  // 傳遞一個整合了各個依賴條件的對象給子類的compile方法
  return this.compile({
    taps: this.taps,
    interceptors: this.interceptors,
    args: this._args,
    type: type
  });
}

從一開始,咱們就在Hook.js上分析,咱們來看看Hook上的compile

compile(options) {
  throw new Error("Abstract: should be overriden");
}

清晰明瞭,這個方法必定要子類複寫,否則報錯,上面的_createCompileDelegate的註釋也寫得很清楚,在當前的上下文中,this指向的是,子類,在咱們這個例子中就是SyncHook

來看看SyncHook 的compile

compile(options) {
  // 如今options 是由Hook裏面 傳到這裏的
  // options
  // {
  //  taps: this.taps, tap對象數組
  //  interceptors: this.interceptors, 攔截器數組
  //  args: this._args,
  //  type: type
  // }
  // 對應回教程中的編譯出來的調用函數依賴於的那幾項看看,是否是這些,鉤子的個數,new SyncHook(['arg'])的參數個數,攔截器的個數,鉤子的類型.
  factory.setup(this, options);

  return factory.create(options);
}

好吧 如今來看看setup, 咦? factory 怎麼來的,原來

const factory = new SyncHookCodeFactory();

是new 出來的

如今來看看SyncHookCodeFactory 的父類 HookCodeFactory

constructor(config) {

  // 這個config做用暫定.由於我看了這個文件,沒看到有引用的地方,
  // 應該是其餘子類有引用到
  this.config = config;
  // 這兩個不難懂, 往下看就知道了
  this.options = undefined;
  this._args = undefined;
}

如今能夠來看一下setup了

setup(instance, options) {
  // 這裏的instance 是syncHook 實例, 其實就是把tap進來的鉤子數組給到鉤子的_x屬性裏.
  instance._x = options.taps.map(t => t.fn);
}

OK, 到create了

這個create有點長, 看仔細了,咱們如今分析同步的部分.

create(options) {
  // 初始化參數,保存options到本對象this.options,保存new Hook(["options"]) 傳入的參數到 this._args
  this.init(options);
  let fn;
  // 動態構建鉤子,這裏是抽象層,分同步, 異步, promise
  switch (this.options.type) {
    // 先看同步
    case "sync":
      // 動態返回一個鉤子函數
      fn = new Function(
        // 生成函數的參數,no before no after 返回參數字符串 xxx,xxx 在
        // 注意這裏this.args返回的是一個字符串,
        // 在這個例子中是options
        this.args(),
        '"use strict";\n' +
          this.header() +
          this.content({
            onError: err => `throw ${err};\n`,
            onResult: result => `return ${result};\n`,
            onDone: () => "",
            rethrowIfPossible: true
          })
      );
      break;
    case "async":
      fn = new Function(
        this.args({
          after: "_callback"
        }),
        '"use strict";\n' +
          this.header() +
          // 這個 content 調用的是子類類的 content 函數,
          // 參數由子類傳,實際返回的是 this.callTapsSeries() 返回的類容
          this.content({
            onError: err => `_callback(${err});\n`,
            onResult: result => `_callback(null, ${result});\n`,
            onDone: () => "_callback();\n"
          })
      );
      break;
    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";
      fn = new Function(this.args(), code);
      break;
  }
  // 把剛纔init賦的值初始化爲undefined
  // this.options = undefined;
  // this._args = undefined;
  this.deinit();

  return fn;
}

到了這個方法,一切咱們都一目瞭然了(看content的參數), 在咱們的例子中他是經過動態的生成一個call方法,根據的條件有,鉤子是否有context 屬性(這個是根據header的代碼才能知道), 鉤子的個數, 鉤子的類型,鉤子的參數,鉤子的攔截器個數.

注意,這上面有關於 fn這個變量的函數,返回的都是字符串,不是函數不是方法,是返回能夠轉化成代碼執行的字符串,思惟要轉變過來.

如今咱們來看看header()

header() {
  let code = "";
  // this.needContext() 判斷taps[i] 是否 有context 屬性, 任意一個tap有 都會返回 true
  if (this.needContext()) {
    // 若是有context 屬性, 那_context這個變量就是一個空的對象.
    code += "var _context = {};\n";
  } else {
    // 不然 就是undefined
    code += "var _context;\n";
  }
  // 在setup()中 把全部tap對象的鉤子 都給到了 instance ,這裏的this 就是setup 中的instance _x 就是鉤子對象數組
  code += "var _x = this._x;\n";
  // 若是有攔截器,在咱們的例子中,就有一個攔截器
  if (this.options.interceptors.length > 0) {
    // 保存taps 數組到_taps變量, 保存攔截器數組 到變量_interceptors
    code += "var _taps = this.taps;\n";
    code += "var _interceptors = this.interceptors;\n";
  }
  // 若是沒有攔截器, 這裏也不會執行.一個攔截器只會生成一次call
  // 在咱們的例子中,就有一個攔截器,就有call
  for (let i = 0; i < this.options.interceptors.length; i++) {
    const interceptor = this.options.interceptors[i];
    if (interceptor.call) {
      // getInterceptor 返回的 是字符串 是 `_interceptors[i]`
      // 後面的before 由於咱們的攔截器沒有context 因此返回的是undefined 因此後面沒有跟一個空對象
      code += `${this.getInterceptor(i)}.call(${this.args({
        before: interceptor.context ? "_context" : undefined
      })});\n`;
    }
  }
  return code;
  // 注意 header 返回的不是代碼,是能夠轉化成代碼的字符串(這個時候並無執行).
  /**
    * 此時call函數應該爲:
    * "use strict";
    * function (options) {
    *   var _context;
    *   var _x = this._x;
    *   var _taps = this.taps;
    *   var _interterceptors = this.interceptors;
    * // 咱們只有一個攔截器因此下面的只會生成一個
    *   _interceptors[0].call(options);
    *}
    */
}

如今到咱們的this.content()了,仔細一看,this.content()方法並不在HookCodeFactory上,很明顯這個content是由子類來實現的,往回看看這個create是由誰調用的?沒錯,是SuncHookCodeFactory的石料理,咱們來看看SyncHook.js上的SyncHookCodeFactory實現的content

在看這個content實現以前,先來回顧一下父類的create()給他傳了什麼參數.

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

注意了,這上面不是拋出錯誤,不是返回值. 這裏面的回調執行了之後返回的是一個字符串,不要搞混了代碼與能夠轉化成代碼的字符串.

content({ onError, onResult, onDone, rethrowIfPossible }) {
  return this.callTapsSeries({
    // 能夠在這改變onError 可是這裏的 i 並無用到,這是什麼操做...
    // 注意這裏並無傳入onResult
    onError: (i, err) => onError(err),
    onDone,
    // 這個默認爲true
    rethrowIfPossible
  });
}

這個函數返回什麼取決於this.callTapSeries(), 那接下來咱們來看看這個函數(這層層嵌套,其實也是有可斟酌的地方.看源碼不只要看實現,代碼的組織也是很重要的編碼能力)

剛纔函數的頭部已經出來了,頭部作了初始化的操做,與生成執行攔截器代碼.content很明顯,要開始生成執行咱們的tap對象的代碼了(若是否則,咱們的tap進來的函數在哪裏執行呢? 滑稽:).

callTapsSeries({ onError, onResult, onDone, rethrowIfPossible }) {
  // 若是 taps 鉤子處理完畢,執行onDone,或者一個tap都沒有 onDone() 返回的是一個字符串.看上面的回顧就知道了.
  if (this.options.taps.length === 0) return onDone();
  // 若是由異步鉤子,把第一個異步鉤子的下標,若是沒有這個返回的是-1
  const firstAsync = this.options.taps.findIndex(t => t.type !== "sync");
  // 定義一個函數 接受一個 number 類型的參數, i 應該是taps的index
  // 從這個函數的命名來看,這個函數應該會遞歸的執行
  // 咱們先開最後的return語句,發現第一個傳進來的參數是0
  const next = i => {
    // 若是 大於等於鉤子函數數組長度, 返回並執行onDone回調,就是tap對象都處理完了
    // 跳出遞歸的條件
    if (i >= this.options.taps.length) {
      return onDone();
    }
    // 這個方法就是遞歸的關鍵,看見沒,逐漸往上遍歷
    // 注意這裏只是定義了方法,並無執行
    const done = () => next(i + 1);
    // 傳入一個值 若是是false 就執行onDone true 返回一個 ""
    // 字面意思,是否跳過done 應該是增長一個跳出遞歸的條件
    const doneBreak = skipDone => {
      if (skipDone) return "";
      return onDone();
    };
    // 這裏就是處理單個taps對象的關鍵,傳入一個下標,和一系列回調.
    return this.callTap(i, {
      // 調用的onError 是 (i, err) => onError(err) , 後面這個onError(err)是 () => `throw ${err}`
      // 目前 i done doneBreak 都沒有用到
      onError: error => onError(i, error, done, doneBreak),
      // 這裏onResult 同步鉤子的狀況下在外部是沒有傳進來的,剛纔也提到了
      // 這裏onResult是 undefined
      onResult:
        onResult &&
        (result => {
          return onResult(i, result, done, doneBreak);
        }),
      // 沒有onResult 必定要有一個onDone 因此這裏就是一個默認的完成回調
      // 這裏的done 執行的是next(i+1), 也就是迭代的處理完全部的taps
      onDone:
        !onResult &&
        (() => {return done();}),
      // rethrowIfPossible 默認是 true 也就是返回後面的
      // 由於沒有異步函數 firstAsync = -1.
      // 因此返回的是 -1 < 0,也就是true, 這個能夠判斷當前的是不是異步的tap對象
      //  這裏挺妙的 若是是 false 那麼當前的鉤子類型就不是sync,多是promise或者是async
      // 具體做用要看callTaps()如何使用這個.
      rethrowIfPossible:
        rethrowIfPossible && (firstAsync < 0 || i < firstAsync)
    });
  };
  
  return next(0);
}

參數搞明白了,如今,咱們能夠進入callTap() 了.

callTap挺長的,由於他也分了3種類型分別處理,像create()同樣.

/** tapIndex 下標
  * onError:() => onError(i,err,done,skipdone) ,
  * onReslt: undefined
  * onDone: () => {return: done()} //開啓遞歸的鑰匙
  * rethrowIfPossible: false 說明當前的鉤子不是sync的.
  */
callTap(tapIndex, { onError, onResult, onDone, rethrowIfPossible }) {
  let code = "";
  // hasTapCached 是否有tap的緩存, 這個要看看他是怎麼作的緩存了
  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) {
        // 這裏getTap返回的是 _taps[0] _taps[1]... 的字符串
        // 這裏生成的代碼就是 `var _tap0 = _taps[0]`
        // 注意: _taps 變量咱們在 header 那裏已經生成了
        code += `var _tap${tapIndex} = ${this.getTap(tapIndex)};\n`;
        // 能夠看到這個變量的做用就是,若是有多個攔截器.這裏也只會執行一次.
        // 注意這句獲取_taps 對象的下標用的是tapIndex,在一次循環中,這個tapIndex不會變
        // 就是說若是這裏執行屢次,就會生成多個重複代碼,不穩定,也影響性能.
        // 可是你又要判斷攔截器有沒有tap才能夠執行,或許有更好的寫法
        // 若是你能想到,那麼你就是webpack的貢獻者了.不過這樣寫,彷佛也沒什麼很差.
        hasTapCached = true;
      }
      // 這裏很明顯跟上面的getTap 同樣 返回的都是字符串
      // 我就直接把這裏的code 分析出來了,注意 這裏仍是在循壞中.
      // code += _interceptor[0].tap(_tap0);
      // 因爲咱們的攔截器沒有context,因此沒傳_context進來.
      // 能夠看到這裏是調用攔截器的tap方法而後傳入tap0對象的地方
      code += `${this.getInterceptor(i)}.tap(${
        interceptor.context ? "_context, " : ""
      }_tap${tapIndex});\n`;
    }
  }
  // 跑出了循壞
  // 這裏的getTapFn 返回的也是字符串 `_x[0]`
  // callTap用到的這些所有在header() 那裏生成了,忘記的回頭看一下.
  // 這裏的code就是: var _fn0 = _x[0]
  code += `var _fn${tapIndex} = ${this.getTapFn(tapIndex)};\n`;
  const tap = this.options.taps[tapIndex];
  // 開始處理tap 對象
  switch (tap.type) {
    case "sync":
      // 全是同步的時候, 這裏不執行, 若是有異步函數,那麼恭喜,有可能會報錯.因此他加了個 try...catch
      if (!rethrowIfPossible) {
        code += `var _hasError${tapIndex} = false;\n`;
        code += "try {\n";
      }
      // 前面分析了 同步的時候 onResult 是 undefined
      // 咱們也分析一下若是走這裏會怎樣
      // var _result0 = _fn0(option)
      // 能夠看到是調用tap 進來的鉤子而且接收參數
      if (onResult) {
        code += `var _result${tapIndex} = _fn${tapIndex}(${this.args({
          before: tap.context ? "_context" : undefined
        })});\n`;
      } else {
        // 因此會走這裏
        // _fn0(options) 額... 我日 有就接受一下結果
        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 就把結果給傳遞出去. 目前沒有
      if (onResult) {
        code += onResult(`_result${tapIndex}`);
      }
      // 有onDone() 就調用他開始遞歸,還記得上面的next(i+1) 嗎?
      if (onDone) {
        code += onDone();
      }
      // 這裏是不上上面的if的大括號,在這個例子中沒有,因此這裏也不執行
      if (!rethrowIfPossible) {
        code += "}\n";
      }
      // 同步狀況下, 這裏最終的代碼就是
      // var _tap0 = _taps[0];
      // _interceptors[0].tap(_tap0);
      // var _fn0 = _x[0];
      // _fn0(options);
      // 能夠看到,這裏會遞歸下去
      // 由於咱們tap了4個鉤子
      // 因此這裏會從復4次
      // 最終長這樣
      // var _tap0 = _taps[0];
      // _interceptors[0].tap(_tap0);
      // var _fn0 = _x[0];
      // _fn0(options);
      // var _tap1 = _taps[1];
      // _interceptors[1].tap(_tap1);
      // var _fn1 = _x[1];
      // _fn1(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 += `var _promise${tapIndex} = _fn${tapIndex}(${this.args({
        before: tap.context ? "_context" : undefined
      })});\n`;
      code += `if (!_promise${tapIndex} || !_promise${tapIndex}.then)\n`;
      code += `  throw new Error('Tap function (tapPromise) did not return promise (returned ' + _promise${tapIndex} + ')');\n`;
      code += `_promise${tapIndex}.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;
}

好了, 到了這裏 咱們能夠把compile 出來的call 方法輸出出來了

"use strict";
function (options) {
  var _context;
  var _x = this._x;
  var _taps = this.taps;
  var _interterceptors = this.interceptors;
// 咱們只有一個攔截器因此下面的只會生成一個
  _interceptors[0].call(options);

  var _tap0 = _taps[0];
  _interceptors[0].tap(_tap0);
  var _fn0 = _x[0];
  _fn0(options);
  var _tap1 = _taps[1];
  _interceptors[1].tap(_tap1);
  var _fn1 = _x[1];
  _fn1(options);
  var _tap2 = _taps[2];
  _interceptors[2].tap(_tap2);
  var _fn2 = _x[2];
  _fn2(options);
  var _tap3 = _taps[3];
  _interceptors[3].tap(_tap3);
  var _fn3 = _x[3];
  _fn3(options);
}

到了這裏能夠知道,咱們的例子中h1.call()其實調用的就是這個方法.到此咱們能夠說是知道了這個庫的百分之80了.

不知道你們有沒有發現,這個生成的函數的參數列表是從哪裏來的呢?往回翻到create()方法裏面調用的this.args()你就會看見,沒錯就是this._args. 這個東西在哪裏初始化呢? 翻一下就知道,這是在Hook.js這個類裏面初始化的,也就是說你h1 = new xxxHook(['options']) 的時候傳入的數組有幾個值,那麼你h1.call({name: 'haha'}) 就能傳幾個值.看教程的時候他說,這裏傳入的是一個參數名字的字符串列表,那時候我就納悶,什麼鬼,我傳入的不是值嗎,怎麼就變成了參數名稱,如今徹底掌握....

好了,最簡單的SyncHook 已經搞掂,可是一看tapable內部核心使用的鉤子卻不是他,而是SyncBailHook,在教程中咱們已經知道,bail是隻要有一個鉤子執行完了,而且返回一個值,那麼其餘的鉤子就不執行.咱們來看看他是怎麼實現的.

從剛纔咱們弄明白的synchook,咱們知道了他的套路,其實生成的函數的header()都是同樣的,此次咱們直接來看看bailhook實現的content()方法

content({ onError, onResult, onDone, rethrowIfPossible }) {
  return this.callTapsSeries({
    onError: (i, err) => onError(err),
  // 看回callTapsSeries 就知道這裏傳入的next 是 done
    onResult: (i, result, next) =>
      `if(${result} !== undefined) {\n${onResult(
        result
      )};\n} else {\n${next()}}\n`,
    onDone,
    rethrowIfPossible
  });
}

看出來了哪裏不同嗎? 是的bailhookcallTapsSeries傳了onResult屬性,咱們來看看他這個onResult是啥黑科技

父類傳的onResult默認是 (result) => 'return ${result}',那麼他這裏返回的就是:

// 下面返回的是字符串,
if (xxx !== undefined) {
  // 這裏說明,只要有返回值(由於不返回默認是undefined),就會當即return;
  return result;
} else {
  // next(); 這裏返回的是一個字符串(由於要生成字符串代碼)
  // 我在上面的註釋中提到了 next 是 done 就是那個開啓遞歸的門
  // 因此若是tap 一直沒返回值, 這裏就會一直 if...else.. 的嵌套下去
  
}

回頭想一想,咱們剛剛是否是分析了capTap(),若是咱們傳了onResult 會怎樣? 若是你還記得就知道,若是有傳了onResult這個回調,他就會接收這個返回值.而且會調用這個回調把result傳出去.

並且還要注意的是,onDonecallTap()的時候是處理過的,我在貼出來一次.

onDone:!onResult && (() => {return done();})

也就是說若是我傳了onResult 那麼這個onDone就是一個false.

因此遞歸的門如今從synconDone,變到syncBailonResult

好,如今帶着這些變化去看this.capTap(),你就能推出如今這個 call 函數會變成這樣.

"use strict";
function (options) {
  var _context;
  var _x = this._x;
  var _taps = this.taps;
  var _interterceptors = this.interceptors;
// 咱們只有一個攔截器因此下面的只會生成一個
  _interceptors[0].call(options);

  var _tap0 = _taps[0];
  _interceptors[0].tap(_tap0);
  var _fn0 = _x[0];
  var _result0 = _fn0(options);

  if (_result0 !== undefined) {
    // 這裏說明,只要有返回值(由於不返回默認是undefined),就會當即return;
    return _result0
  } else {
    var _tap1 = _taps[1];
    _interceptors[1].tap(_tap1);
    var _fn1 = _x[1];
    var _result1 = _fn1(options);
    if (_result1 !== undefined) {
      return _result1
    } else {
      var _tap2 = _taps[2];
      _interceptors[2].tap(_tap2);
      var _fn2 = _x[2];
      var _result2 = _fn2(options);
      if (_result2 !== undefined) {
        return _result2
      } else {
        var _tap3 = _taps[3];
        _interceptors[3].tap(_tap3);
        var _fn3 = _x[3];
        _fn3(options);
      }
    }
  }

到現在,tapable庫 已經刪除了 tapable.js文件(可能作了一些整合,更細分了),只留下了鉤子文件.但不影響功能,webpack 裏的compile compilation 等一衆重要插件,都是基於 tapable庫中的這些鉤子.

如今咱們require('tapable')獲得的對象是這樣的:

{
    SyncHook: function(...){},
    SyncBailHook: function(...){},
    ...
}

到此,關於tapable的大部分我都解剖了一遍,還有其餘類型的hook 若是大家願意,相信大家去研究一下,也可以遊刃有餘.

那個,寫得有些隨性,可能會讓大家以爲模糊,可是...我真盡力了,這篇改了幾遍,歷時一個星期...,不懂就在那個評論區問我.我看到會回覆的.共勉.

後記:
原本覺得會很難,可是越往下深刻的時候發現,大神之因此成爲大神,不是他的代碼寫得牛,是他的思惟牛,沒有看不懂的代碼,只有跟不上的思路,要看懂他如何把call 函數組織出來不難,難的是,他竟然能想到這樣來生成函數,還能夠考慮到,攔截器鉤子,和context 屬性,以及他的 onResult onDone 回調的判斷,架構的設計,等等,一步接一步.先膜拜吧...

路漫漫其修遠兮, 吾將上下而求索.

相關文章
相關標籤/搜索