Webpack Hook雜談

Tapable

const {
	SyncHook,
	SyncBailHook,
	SyncWaterfallHook,
	SyncLoopHook,
	AsyncParallelHook,
	AsyncParallelBailHook,
	AsyncSeriesHook,
	AsyncSeriesBailHook,
	AsyncSeriesWaterfallHook
 } = require("tapable");
複製代碼

Tapable提供了上述9中hook。詳細的api方法能夠查看Tapable文檔javascript

Tapable主要由兩個重要部分組成java

  1. Hook
  2. HookCodeFactory

下面以SyncHook爲例,咱們看看Hook處理的整個流程。SyncHook是Tapable中最容易理解的Hook,所以做爲Demo進行分析。webpack

Demo代碼以下:git

class Car {
  constructor() {
    this.hooks = {
       accelerate: new SyncHook(["newSpeed"]),
    };
  }
  
  setSpeed(newSpeed) {
    this.hooks.accelerate.call(newSpeed);
  }
}

const myCar = new Car();

myCar.hooks.accelerate.tap("LoggerPlugin", newSpeed1 => {
  console.log(`${newSpeed1}`)
});

myCar.hooks.accelerate.tap("LoggerPlugin2", newSpeed2 => {
  console.log(`${newSpeed2}`)
})

myCar.hooks.accelerate.tap("LoggerPlugin3", newSpeed3 => {
  console.log(`${newSpeed3}`)
})

myCar.setSpeed(100);
複製代碼

註冊插件

myCar.hooks.accelerate.tap("LoggerPlugin", newSpeed1 => {
  console.log(`${newSpeed1}`)
});
複製代碼

首先咱們一塊兒看看tap方法(代碼通過部分刪減和轉換)。github

tap(options, fn) {
  if (typeof options === "string") options = { name: options };
  options = Object.assign({ type: "sync", fn: fn }, options);
  this.taps.push(item);
}
複製代碼

tap方法主要是把輸入的兩個參數(plugin的名稱plugin的主要邏輯)組成一個帶有type的對象,而後存放到taps數組中。web

taps數組中存放的對象以下所示。api

{
    name: "LoggerPlugin1",
    type: "sync",
    fn: (newSpeed1) => {
        console.log(`${newSpeed1}`)
    }
}
複製代碼

Hook的觸發

接下來咱們一塊兒看看hook的call方法作了些什麼。數組

this.hooks.accelerate.call(newSpeed)
複製代碼

其實call是一個閉包。完成了把註冊好的plugin按照必定的規則執行。而這個執行的規則則是由_createCall建立。_createCall會調用compile方法,compile是由Hook的子類進行實現(這裏就是由SyncHook來實現)。閉包

class Hook {
  constructor(args) {
    if (!Array.isArray(args)) args = [];
      this._args = args;
      this.taps = [];
      this.call = this._call;
      this._x = undefined;
  }

  _createCall(type) {
    return this.compile({
      taps: this.taps,
      args: this._args,
      type: type
    });
  }
	
  tap(options, fn) {...}
  ...
}

function createCompileDelegate(name, type) {
  return function lazyCompileHook(...args) {
    this[name] = this._createCall(type);
    return this[name](...args);
  };
}

Object.defineProperties(Hook.prototype, {
  _call: {
    value: createCompileDelegate("call", "sync"),
    configurable: true,
    writable: true
  },
})
複製代碼

每一個Hook都有一個對應的HookCodeFactoryHookCodeFactory的做用就是建立一個根據規則建立待執行plugin的函數。HookCodeFactory裏面大部分代碼是都是在拼接函數。異步

const factory = new SyncHookCodeFactory();

class SyncHook extends Hook {
  compile(options) {
    factory.setup(this, options);
    return factory.create(options);
  }
}
複製代碼

如下我將簡化SyncHookCodeFactory代碼,代碼和源代碼並不一致,只是爲了說明code是怎樣生成的。

HookCodeFactory,是用動態Function構建Hook觸發的Plugin執行方法。

爲何要用new Function?

由於create的過程是動態的,不可能預先寫好方法,所以用動態的Function也是一種解決方案。

class SyncHookCodeFactory {
  constructor() {
    this.options = undefined;
    this._args = [];
  }

  create(options) {
    this.init(options);
    const fn = new Function(
      this.args(),
      this.content()
    );
    return fn;
  }

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

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

  content() {
    let code = '"use strict";\nvar _x = this._x;\n';
    if (this.options.taps.length === 0) { return code; }
    for (let j = this.options.taps.length - 1; j >= 0; j--) {
      code += `var _fn${j} = ${this.getTapFn(j)};\n`;
      code += `_fn${j}(${this.args()});\n`;
    }
    return code;
  }

  args() {
    return this._args.join(', ');
  }

  getTapFn(idx) {
    return `_x[${idx}]`;
  }
}
複製代碼

在本例子中(串行鉤子),執行factory的create方法後,會返回一個函數,參數即爲call方法傳入的參數:

function anonymous(newSpeed) {
 "use strict";
    var _context;
    var _x = this._x;
    var _fn0 = _x[0];
    _fn0(newSpeed);
    var _fn1 = _x[1];
    _fn1(newSpeed);
    var _fn2 = _x[2];
    _fn2(newSpeed);
}
複製代碼

須要說一下,這裏的_x,其實由taps.map(t => t.fn)獲得的。簡單來講就是註冊的plugin列表。 下面簡單地把_x數組所表明的內容列出來。

// 以_x[0]爲例子
_x[0] = newSpeed1 => {
  console.log(`${newSpeed1}`)
}
複製代碼

AsyncParallelHook與AsyncSeriesHook

由於在一篇博文中看到, AsyncParallelHookAsyncSeriesHook兩個執行異步的方法(文中是settimeout),執行時間是不一致的。AsyncParallelHook和它名字同樣,是並行執行的;相反AsyncSeriesHook是串行執行的。

因爲名字都是帶async的,給人的錯覺是都是異步並行。因而作了Demo驗證一下。

class Car {
  constructor() {
    this.hooks = {
      // 這裏是AsyncParallelHook與AsyncSeriesHook切換
      // calculateRoutes: new AsyncParallelHook(["name"])
      calculateRoutes: new AsyncSeriesHook(["name"])
    };
  }
  useNavigationSystemAsync(name) {
    this.hooks.calculateRoutes.callAsync(name, err => {
      console.log(err);
    });
  }
}

const myCar = new Car();

myCar.hooks.calculateRoutes.tapAsync("TapAsync1", (name, cb) => {
  console.log(name, 1);
  cb();
});

myCar.hooks.calculateRoutes.tapAsync("TapAsync2", (name, cb) => {
  console.log(name, 2);
  cb();
});

myCar.useNavigationSystemAsync('webpack')
複製代碼

AsyncSeriesHookFactory產生的代碼以下

function anonymous(name, _callback) {
 "use strict";
    function _next0() {
      const _fn1 = _x[1];
      _fn1(name, _err1 => {
        if (_err1) {
          _callback(_err1);
        } else {
          _callback();
        }
      });
    }
    const _fn0 = _x[0];
    _fn0(name, _err0 => {
      if (_err0) {
        _callback(_err0);
      } else {
        _next0();
      }
    });
}
複製代碼

AsyncParallelHookFactory產生的代碼以下

function anonymous(name, _callback) {
 "use strict";
    do {
      var _counter = 2;
      var _done = () => {
        _callback();
      };
      if (_counter <= 0) { break; }
      const _fn0 = _x[0];
      _fn0(name, _err0 => {
        if (_err0) {
          if (_counter > 0) {
            _callback(_err0);
            _counter = 0;
          }
        } else if (--_counter === 0) { _done(); }
      });
      if (_counter <= 0) { break; }
      const _fn1 = _x[1];
      _fn1(name, _err1 => {
        if (_err1) {
          if (_counter > 0) {
            _callback(_err1);
            _counter = 0;
          }
        } else if (--_counter === 0) { _done(); }
      });
      if (_counter <= 0) { break; }
    } while (false);
}
複製代碼

首先,咱們能夠得知若是在callback中傳入參數,後續的插件都都不會執行。

  • AsyncSeriesHookFactory 能夠看到,執行完_fn0(即第一個插件)後,纔會調用_next0()執行_fn1
  • AsyncParallelHookFactory 則不一樣,全部的函數幾乎是同時執行,每一個回調執行完count減一,直到count爲0執行done方法(done方法就是下面的這個)
err => {
    console.log(err);
}
複製代碼

區別於EventEmitter

Tapable的寫法與傳統的事件驅動機制不太同樣,但它作的事情都是差很少。都是須要有一個訂閱「事件」方法,和觸發「事件」方法。
雖說機制比較類似,但提供了9種基本的觸發策略的Tapable能夠說更增強大。

類似處

先說說它們之間類似的地方,以SyncHook爲例來對比的話,SyncHook基本能夠用EventEmitter實現。

Tapable的tap做用至關於EventEmitter的on;而call做用就至關於emit

// SyncHook
const accelerateHook = new SyncHook(["newSpeed"])

accelerateHook.tap("LoggerPlugin", newSpeed => {
  console.log(`${newSpeed}`)
});

accelerateHook.call(100);

// Node EventEmitter
const eventEmitter = new EventEmitter();

eventEmitter.on("accelerate", newSpeed1 => {
  console.log(`${newSpeed1}`)
});

eventEmitter.emit("accelerate", 100);
複製代碼

不一樣點

EventEmitter事件訂閱者之間是無感知的,相互沒法影響的。WebpackTapable的事件訂閱者之間便可以是無感知也能夠是相互影響。

舉個例子說明,好比SyncWaterfallHook中前一個訂閱者的回調返回值會做爲後一個訂閱者的輸入參數。

const swfh = new SyncWaterfallHook(['param']);

swfh.tap('a', function (param) {
  console.log(param);
  return param + 1;
});
swfh.tap('b', function (param) {
  console.log(param);
  return param + 2;
});
swfh.tap('c', function (param) {
  console.log(param);
});

swfh.call(1);

// console
/* 1 2 4 */
複製代碼

不只如此,Tapable還提供InterceptionContextHookMapMultiHook等玩法。

相關文章
相關標籤/搜索