多是全網最全最新最細的 webpack-tapable-2.0 的源碼分析

tapable (2.0.0-beta 版本)

以前分析了 tapable 0.2.8 版本的源碼,看起來很好懂,可是也存在一些缺點,就是沒法明確地知道 plugin 是屬於同步、仍是異步,沒法更加細粒度的管理這些 handler,並且關於 async 的插件都是採用遞歸的方式,天然內存的佔用就很大,javascript

可是 tapable 2.0.0-beta 版本的重構,猶如藝術品通常,讓人驚豔。源碼內部採用 getter 惰性加載與緩存的方式,以及利用 new Function 去消除遞歸調用。php

消除遞歸調用的方式就是在第一次調用 call 的時候,經過字符串拼接可執行的字符串代碼(源碼內部稱之爲 compile),經過 new Function 來生成 fn,而且緩存下來。這樣的做用就是將遞歸代碼非遞歸化,能減小內存的消耗。html

先來張圖,直觀感覺下 Tapable 的架構,爲何稱之爲藝術。vue

能夠看出 Tabable 重構以後多了一個 Hook 的概念,有同步鉤子,異步串行鉤子,異步並行鉤子等。每種鉤子都是一個類,它們都是繼承於 Hook 基類。闡述下各類 Hook 類的做用。java

Hook 類

名稱 鉤入的方式 做用
Hook taptapAsynctapPromise 鉤子基類
SyncHook tap 同步鉤子
SyncBailHook tap 同步鉤子,只要執行的 handler 有返回值,剩餘 handler 不執行
SyncLoopHook tap 同步鉤子,只要執行的 handler 有返回值,一直循環執行此 handler
SyncWaterfallHook tap 同步鉤子,上一個 handler 的返回值做爲下一個 handler 的輸入值
AsyncParallelBailHook taptapAsynctapPromise 異步鉤子,handler 並行觸發,可是跟 handler 內部調用回調函數的邏輯有關
AsyncParallelHook taptapAsynctapPromise 異步鉤子,handler 並行觸發
AsyncSeriesBailHook taptapAsynctapPromise 異步鉤子,handler 串行觸發,可是跟 handler 內部調用回調函數的邏輯有關
AsyncSeriesHook taptapAsynctapPromise 異步鉤子,handler 串行觸發
AsyncSeriesLoopHook taptapAsynctapPromise 異步鉤子,能夠觸發 handler 循環調用
AsyncSeriesWaterfallHook taptapAsynctapPromise 異步鉤子,上一個 handler 能夠根據內部的回調函數傳值給下一個 handler

Hook Helper 與 Tapable 類

名稱 做用
HookCodeFactory 編譯生成可執行 fn 的工廠類
HookMap Map 結構,存儲多個 Hook 實例
MultiHook 組合多個 Hook 實例
Tapable 向前兼容老版本,實例必須擁有 hooks 屬性

簡單上手

tapable 2.0.0-beta 版本的使用跟以前分析的 0.2.8 版本徹底不一樣,可是實現的功能,以及原理是一致的。webpack

const { SyncHook } = require('tapable')

// 實例化 SyncHook
const sh = new SyncHook(['arg1'])

// 經過 tap 註冊 handler
sh.tap('1', function (arg1, arg2) {
    console.log(arg1, arg2, 1);
});
sh.tap({
  name: '2',
  before: '1',
}, function (arg1) {
    console.log(arg1, 2);
});
sh.tap({
  name: '3',
  stage: -1,
}, function (arg1) {
    console.log(arg1, 3);
});

// 經過 call 執行 handler
sh.call('tapable', 'tapable-2.0.0')

// 打印順序以下
tapable, 3
tapable, 2
tapable, undefined, 1
複製代碼

如上所述,實例化 SyncHook 的時候接收字符串數組。它的長度會影響你經過 call 方法調用 handler 時入參個數。就像例子所示,調用 call 方法傳入的是兩個參數,實際上 handler 只能接收到一個參數,由於你在 new SyncHook 的時候傳入的字符串數組長度是1。SyncHook 對象是經過 tap 方法去註冊 handler的,第一個參數必須是字符串或者對象,其實即便是字符串,也會在內部轉成對象,變成以下結構:git

interface Tap {
  name: string, // 標記每一個 handler,必須有
  before: string | array, // 插入到指定的 handler 以前
	type: string, // 類型:'sync', 'async', 'promise'
	fn: Function, // handler
	stage: number, // handler 順序的優先級,默認爲 0,越小的排在越前面執行
	context: boolean // 內部是否維護 context 對象,這樣在不一樣的 handler 就能共享這個對象
}
複製代碼

由於我 name 爲 2 的 handler 註冊的時候,是傳了一個對象,它的 before 屬性爲 1,說明這個 handler 要插到 name 爲 1 的 handler 以前執行,所以打印的順序在第二位,可是又由於 name 爲 3 的 handler 註冊的時候,stage 屬性爲 -1,比其餘的 handler 的 stage 要小,因此它會被移到最前面執行。github

探索原理

那麼既然咱們從 SyncHook 這個最簡單的鉤子類入手,也知道了如何使用,那麼咱們從源碼的角度來感覺下 Tapable 重構版猶如藝術版的架構設計吧。找到入口 tapable/index.jsweb

exports.__esModule = true;
exports.Tapable = require("./Tapable");
exports.SyncHook = require("./SyncHook");
exports.SyncBailHook = require("./SyncBailHook");
exports.SyncWaterfallHook = require("./SyncWaterfallHook");
exports.SyncLoopHook = require("./SyncLoopHook");
exports.AsyncParallelHook = require("./AsyncParallelHook");
exports.AsyncParallelBailHook = require("./AsyncParallelBailHook");
exports.AsyncSeriesHook = require("./AsyncSeriesHook");
exports.AsyncSeriesBailHook = require("./AsyncSeriesBailHook");
exports.AsyncSeriesWaterfallHook = require("./AsyncSeriesWaterfallHook");
exports.HookMap = require("./HookMap");
exports.MultiHook = require("./MultiHook");
複製代碼

各類鉤子類以及鉤子輔助類都掛載在對應的屬性上。咱們先來看 SyncHook設計模式

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,而且原型上重寫了 tapAsync、tapPromise、compile 三個方法,也就是 SyncHook 不支持經過 tapAsync 與 tapPromise 來註冊 handler 的,由於它內部的邏輯是不支持異步的。compile 方法是用來編譯生成對應的 fn,而調用 call 方法,其實就是執行了編譯生成的 fn。這個是後話,咱們先來看下 Hook 類的實現,全部的鉤子都是繼承於 Hook 基類。

const util = require("util");

const deprecateContext = util.deprecate(() => {},
"Hook.context is deprecated and will be removed");

class Hook {
	constructor(args) {
		if (!Array.isArray(args)) args = []; // args 必須是數組
    this._args = args;
		this.taps = []; // 存放每次執行 tap 方法的生成的 options 對象
    this.interceptors = []; //存放攔截器
    /** * 如下三種方法都是惰性加載,再執行一次以後,會緩存編譯的 fn, * 只有在加入新 handler 的狀況下,纔會從新編譯,緩存編譯生成的新 fn * 而 fn 其實函數體內將以前版本遞歸部分都磨平了,這樣會減小內存的消耗。 **/
    // 提供 call 方法,執行 sync handler
    this.call = this._call;
    // 提供 promise 方法,執行 promise handler
    this.promise = this._promise;
    // 提供 callAsync 方法,執行 async handler
    this.callAsync = this._callAsync;
    // 會在編譯的 setup 期間過濾 this.taps 獲得全部的 handler 組成的數組
		this._x = undefined;
	}

  // 全部子類都必須重寫編譯方法,由於每一個 Hook 子類都有本身的 compile rules。
	compile(options) {
		throw new Error("Abstract: should be overriden");
	}

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

  // 註冊 'sync' fn
	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)"
			);
		if (typeof options.name !== "string" || options.name === "")
			throw new Error("Missing name for tap");
		if (typeof options.context !== "undefined") deprecateContext();
		options = Object.assign({ type: "sync", fn: fn }, options);
		options = this._runRegisterInterceptors(options);
		this._insert(options);
  }
  
  // 註冊 'async' fn
	tapAsync(options, fn) {
		if (typeof options === "string") options = { name: options };
		if (typeof options !== "object" || options === null)
			throw new Error(
				"Invalid arguments to tapAsync(options: Object, fn: function)"
			);
		if (typeof options.name !== "string" || options.name === "")
			throw new Error("Missing name for tapAsync");
		if (typeof options.context !== "undefined") deprecateContext();
		options = Object.assign({ type: "async", fn: fn }, options);
		options = this._runRegisterInterceptors(options);
		this._insert(options);
	}

  // 註冊 'promise' fn
	tapPromise(options, fn) {
		if (typeof options === "string") options = { name: options };
		if (typeof options !== "object" || options === null)
			throw new Error(
				"Invalid arguments to tapPromise(options: Object, fn: function)"
			);
		if (typeof options.name !== "string" || options.name === "")
			throw new Error("Missing name for tapPromise");
		if (typeof options.context !== "undefined") deprecateContext();
		options = Object.assign({ type: "promise", fn: fn }, options);
		options = this._runRegisterInterceptors(options);
		this._insert(options);
	}

  // 每次執行 tap 的時候,傳入的 options 都要通過 interceptor.register 函數的邏輯。
	_runRegisterInterceptors(options) {
		for (const interceptor of this.interceptors) {
			if (interceptor.register) {
				const newOptions = interceptor.register(options);
				if (newOptions !== undefined) {
					options = newOptions;
				}
			}
		}
		return options;
	}

	withOptions(options) {
		const mergeOptions = opt =>
			Object.assign({}, options, typeof opt === "string" ? { name: opt } : opt);

		// Prevent creating endless prototype chains
		options = Object.assign({}, options, this._withOptions);
		const base = this._withOptionsBase || this;
		const newHook = Object.create(base);

		newHook.tap = (opt, fn) => base.tap(mergeOptions(opt), fn);
		newHook.tapAsync = (opt, fn) => base.tapAsync(mergeOptions(opt), fn);
		newHook.tapPromise = (opt, fn) => base.tapPromise(mergeOptions(opt), fn);
		newHook._withOptions = options;
		newHook._withOptionsBase = base;
		return newHook;
	}

	isUsed() {
		return this.taps.length > 0 || this.interceptors.length > 0;
	}

  // 註冊攔截器
	intercept(interceptor) {
		this._resetCompilation();
		this.interceptors.push(Object.assign({}, interceptor));
		if (interceptor.register) {
			for (let i = 0; i < this.taps.length; i++) {
				this.taps[i] = interceptor.register(this.taps[i]);
			}
		}
	}

  // 每次註冊新 handler,要從新編譯
	_resetCompilation() {
		this.call = this._call;
		this.callAsync = this._callAsync;
		this.promise = this._promise;
	}
  // 插入 tap 對象,可能根據 before,stage 屬性,調整 handler 的執行順序
	_insert(item) {
		this._resetCompilation();
		let before;
		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;
    // 根據 before,stage 屬性,調整 handler 的執行順序
		while (i > 0) {
			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;
		}
		this.taps[i] = item;
	}
}

function createCompileDelegate(name, type) {
	return function lazyCompileHook(...args) {
    // 從新賦值 this.call, this.promise, this.callAsync
    // 由於第一個調用 call 的時候,會走到 _createCall 去 compile,生成 fn
    // 可是第二次調用 call 的時候,fn 已經賦值給了 this.call 了,不須要走到 compile 的邏輯了。
		this[name] = this._createCall(type);
		return this[name](...args);
	};
}

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
	}
});

module.exports = Hook;
複製代碼

能夠看到,Hook 提供了 tap、tapAsync、tapPromise 來註冊 handler,經過了 call、callAsync、promise 三種方式來調用 handler,同時內部還對這三種調用方式作了惰性求值,而且會緩存編譯結果直到注入了新 handler。

分析完 Hook 類的大體功能,咱們再回到 SyncHook 類。發現 compile 方法裏面 new SyncHookCodeFactory。從字面上的理解就是生成同步鉤子代碼的工廠類,它繼承於 HookCodeFactory 類。那麼分析下 HookCodeFactory.js

/* MIT License http://www.opensource.org/licenses/mit-license.php Author Tobias Koppers @sokra */
"use strict";

class HookCodeFactory {
	constructor(config) {
		this.config = config;
		this.options = undefined;
		this._args = undefined;
	}

	create(options) {
		this.init(options);
		let fn;
		switch (this.options.type) {
			case "sync":
				fn = new Function(
					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() +
						this.content({
							onError: err => `_callback(${err});\n`,
							onResult: result => `_callback(null, ${result});\n`,
							onDone: () => "_callback();\n"
						})
				);
				break;
			case "promise":
				......
				fn = new Function(this.args(), code);
				break;
		}
		this.deinit();
		return fn;
	}

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

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

	deinit() {
		this.options = undefined;
		this._args = undefined;
	}

	header() {
		let code = "";
		......
		return code;
	}

	needContext() {
		for (const tap of this.options.taps) if (tap.context) return true;
		return false;
	}

	callTap(tapIndex, { onError, onResult, onDone, rethrowIfPossible }) {
		......
		return code;
	}

	callTapsSeries({ onError, onResult, onDone, rethrowIfPossible }) {
    ......
	}

	callTapsLooping({ onError, onDone, rethrowIfPossible }) {
		......
	}

	callTapsParallel({
		onError,
		onResult,
		onDone,
		rethrowIfPossible,
		onTap = (i, run) => run()
	}) {
		......
		return code;
	}

	args({ before, after } = {}) {
		......
	}

  ......
}

module.exports = HookCodeFactory;

複製代碼

HookCodeFactory 的原型上有不少方法,可是千萬不要慌,也不要畏懼。若是看不懂代碼,咱們能夠一步步 debugger 去調試。

SyncHook 在執行 compile 的時候會調用 HookCodeFactory 的 setup、create 方法,咱們先來看下這兩個方法

setup(instance, options) {
  // 過濾出傳入的 handler
  instance._x = options.taps.map(t => t.fn);
}
init(options) {
  this.options = options;
  this._args = options.args.slice();
}
deinit() {
  this.options = undefined;
  this._args = undefined;
}
create(options) {
  // 獲取調用方 new SyncHook(options)
  this.init(options);
  let fn;
  // 判斷 handler 的類型,經過 new Function 將字符串變成 fn
  switch (this.options.type) {
    case "sync":
      fn = new Function(
        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() +
          this.content({
            onError: err => `_callback(${err});\n`,
            onResult: result => `_callback(null, ${result});\n`,
            onDone: () => "_callback();\n"
          })
      );
      break;
    case "promise":
      ......
      fn = new Function(this.args(), code);
      break;
  }
  // 重置參數,由於 SyncHook 類保存的是一份 HookCodeFactory 類的實例,因此每次編譯完,爲了防止影響 其餘SyncHook 實例。
  this.deinit();
  // 返回編譯生成的函數
  return fn;
}
複製代碼

從執行的邏輯來看,就是先從 taps 裏面過濾出 handler,而後根據類型來生成對應的 fn。因此咱們在調用 call、callAsync、promise 的時候,執行的就是編譯生成的 fn,而且把參數傳入。

上面的例子是用到的 SyncHook,只會走到 case "sync" 的邏輯,咱們重點分析如何生成 fn 的,其他的也是依葫蘆畫瓢。

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

那咱們從下面三個步驟來看:

  • 生成 fn 的形參

    args({ before, after } = {}) {
      let allArgs = this._args;
      if (before) allArgs = [before].concat(allArgs);
      if (after) allArgs = allArgs.concat(after);
      if (allArgs.length === 0) {
        return "";
      } else {
        return allArgs.join(", ");
      }
    }
    複製代碼

    根據實例化 SyncHook 傳入的參數以逗號拼接形參字符串。支持 before 與 after 屬性,可以在字符串的頭部或者尾部插入對應的屬性值字符串。好比 new SyncHook(['arg1', 'arg2']),那麼通過 this.args 處理後,就變成 "arg1, arg2"。再經過 fn = new Function("arg1, arg2") 以後,就變成 fn 接收 arg1 與 arg2兩個形參了。假如你在使用 call 方法的時候傳入三個參數,那麼第三個參數就獲取不到了,由於 fn 只支持兩個參數。

  • 生成 fn 函數體的頭部代碼字符串

    header() {
      let code = "";
      // tap 的時候傳入了 {context: true}
      if (this.needContext()) {
        code += "var _context = {};\n";
      } else {
        code += "var _context;\n";
      }
      code += "var _x = this._x;\n";
      if (this.options.interceptors.length > 0) {
        code += "var _taps = this.taps;\n";
        code += "var _interceptors = this.interceptors;\n";
      }
      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}]`;
    }
    複製代碼

    header 函數主要是生成頭部的一些參數,能夠看到若是經過 tap、tapPromise、tapAsync 註冊 handler的時候傳入了 context: true,那麼會生成 _context 對象,而且會將 _context 傳入每個 handler,由於這是個對象引用,因此對於每一個 handler 來講,實際上是共享了一份 _context 對象。同時 Hook 是支持經過 intercept 方法註冊攔截器的,該方法接收一個對象做爲入參,該對象都會保存在鉤子實例的 interceptors 數組。數據結構以下:

    interface HookInterceptor {
      call: (context?, ...args) => void, // 還未開始執行 handler 以前執行
      loop: (context?, ...args) => void,
      tap: (context?, tap: Tap) => void, // 插入一個 handler
      register: (tap: Tap) => Tap, // 改變 tap 對象
      context: boolean
    }
    複製代碼

    從接口來看,咱們能夠經過 intercept 方法來插入本身的邏輯,不只能夠註冊 handler 還能夠改變 tap 對象,這樣使得鉤子變得更靈活,更有彈性。

  • 生成 fn 函數體的中間執行代碼的字符串

    看完了 header 的邏輯,咱們再來看 content 的邏輯,由於 content 對於每種鉤子的代碼生成都不同,因此是在對應的鉤子生成的工廠類上作了覆蓋,那麼對於 SyncHook 而言,content 是在 SyncHookCodeFactory 這個工廠類重寫了 content 方法。

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

    能夠看到 SyncHookCodeFactory 這個類的 content 方法是接收一個對象,而且內部又調用了 HookCodeFactory 類上的 callTapsSeries 方法,同時將 onError、onDone、rethrowIfPossible 傳入了。咱們看下 callTapsSeries 的定義。

    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);
    }
    複製代碼

    從上面能夠看出函數內部維護了一個 next 函數,next 函數內部會調用 callTap,而 callTap 內部會在合適的時機調用 done,那麼又會走到 next 函數,那麼這樣就造成了自執行的機制,而函數退出的條件就是遍歷了全部的 this.options.taps 以後,這個數據是維護了咱們經過 tap、tapPromise、tapAsync 註冊 handler 的信息。

阻力與尋找解決辦法。

從上面剖析 SyncHook 源碼的結果來看,尤爲是 compile 那塊涉及到拼接字符串,經過 new Function 生成 fn。這一塊可閱讀性比較差,因此咱們以具體的 Hook 類的使用場景,來覆蓋源碼的每一個步驟,一步步調試。

同步鉤子案例大全

全部的同步鉤子只支持 tap 方法來註冊 sync handler。

syncHook(同步鉤子)

const { SyncHook } = require('tapable')

// 實例化 SyncHook
const sh = new SyncHook(['arg1'])

// 經過 tap 註冊 handler
sh.tap('1', function (arg1, arg2) {
    console.log(arg1, arg2, 1);
});
sh.tap({
  name: '2',
  before: '1',
}, function (arg1) {
    console.log(arg1, 2);
});
sh.tap({
  name: '3',
  stage: -1,
}, function (arg1) {
    console.log(arg1, 3);
});

// 經過 call 執行 handler
sh.call('tapable', 'tapable-2.0.0')

// 打印順序以下
tapable, 3
tapable, 2
tapable, undefined, 1
複製代碼
  1. tap 的源碼分析

    • 先校驗 options 參數的格式,再走到 _runRegisterInterceptors 方法,這一步是爲了執行攔截器的 register 方法,來改變 options。
    • 接着走到 _insert 內部,內部根據 before、stage 屬性來調整 handler 的順序,而且將全部的信息保存到 taps 數組裏面。
  2. call 的源碼分析

    • 執行 call,就是執行了原型上的 _call,也就是執行了 createCompileDelegate,這個函數返回的是另一個 lazyCompileHook 函數,在 lazyCompileHook 函數內部會從新賦值 call 方法,獲得編譯後的結果。也就是第二次調用 call的時候,其實就是執行 _createCall 方法的返回值。
    • _createCall 內部執行了 compile 方法,這個方法在 SyncHook 的原型上。compile 的內部先執行 SyncHookCodeFactory 上的 setup 方法,而後執行 create 方法。
    • setup 與 create 方法都是在 HookCodeFactory 的原型上,由於 SyncHookCodeFactory 是繼承於 HookCodeFactory。
    • setup 內部的邏輯很簡單,就是從 taps 數組過濾出傳入的 handler。
    • create 內部先初始化 options 參數,這個是在調用 compile 的時候傳入的,而後經過字符串拼接執行 new Function 獲得 fn,最後執行的也是這個 fn。

咱們通常對 new Function 很陌生,因此很好奇 create 裏面究竟是生成了什麼。能夠在 new Function 打個斷點,一步步 debugger 一下,最後會發現生成的 fn 是以下的函數。

(function anonymous(arg1) {
  // header
 "use strict";
  var _context;
  var _x = this._x;
  // content
  var _fn0 = _x[0];
  _fn0(arg1);
  var _fn1 = _x[1];
  _fn1(arg1);
  var _fn2 = _x[2];
  _fn2(arg1);
})

// arg1 參數,其實就是在 new Function 時候調用 this.args 生成的字符串而來的,而 this.args 是由實例化鉤子傳入的
// header 塊,this.header 生成的 (HookCodeFactory 原型上)
// content 塊,this.content 生成的 (這個方法會在對應的鉤子工廠類的原型上重寫)
複製代碼

而執行 sh.call('tapable', 'tapable-2.0.0'),其實執行的就是上述的函數,那麼這個庫的做者處心積慮的這麼作的意義何在呢,固然這個例子也看不出很大的做用,只能看到函數體內部沒有 for 循環,函數的執行都是扁平的。最大的好處其實在於你看過 Async*Hook 編譯出來的 fn,你就知道爲啥要這麼作了。

SyncBailHook(同步保險鉤子)

const sbh = new SyncBailHook(['arg1'])
sbh.tap({ 
  context: true, 
  name: '1'
}, function (context, arg1) {
  console.log(context, arg1, 1)
  return 1
});
sbh.tap({
  name: '2',
}, function (arg1) {
  // 不會執行
  console.log(arg1, 2)
});

sbh.call('tapable')

// 打印
{}, tapable, 1
複製代碼

編譯的 fn 以下

(function anonymous(arg1) {
 "use strict";
    var _context = {};
    var _x = this._x;
    var _fn0 = _x[0];
    var _result0 = _fn0(_context, arg1);
    if (_result0 !== undefined) {
        return _result0;;
    } else {
        var _fn1 = _x[1];
        var _result1 = _fn1(arg1);
        if (_result1 !== undefined) {
            return _result1;;
        } else {}
    }

})
複製代碼

SyncBailHook 從字面上的意思是同步保險鉤子,也就是隻要前面的 handler 返回值不是 undefined,下一個 handler 就不會被觸發。

SyncLoopHook(同步循環鉤子)

const slh = new SyncLoopHook()
// 由於 handler 返回值不爲 undefined,會一直循環執行
slh.tap('1', () => {
  console.log(1)
  return 1
})
slh.tap('2', () => {
  console.log(2)
  return 2
})
slh.call()
複製代碼

編譯的 fn 以下

(function anonymous() {
 "use strict";
    var _context;
    var _x = this._x;
    var _loop;
    do {
        _loop = false;
        var _fn0 = _x[0];
        var _result0 = _fn0();
        if (_result0 !== undefined) {
            _loop = true;
        } else {
            var _fn1 = _x[1];
            var _result1 = _fn1();
            if (_result1 !== undefined) {
                _loop = true;
            } else {
                if (!_loop) {}
            }
        }
    } while ( _loop );
})
複製代碼

SyncLoopHook 從字面上的意思是同步循環鉤子,也就是隻要前面的 handler 返回值不是 undefined,那麼會一直循環執行。

SyncWaterfallHook(同步瀑布鉤子)

// SyncWaterfallHook 必須傳入一個長度不爲 0 的數組
const swfh = new SyncWaterfallHook(['arg'])
swfh.tap('1', (arg) => {
  console.log(arg)
  return 1
})
swfh.tap('2', (arg) => {
  console.log(arg)
  return 2
})
swfh.call('webpack')

// 打印以下
webpack
1
複製代碼

編譯的 fn 以下

(function anonymous(arg) {
 "use strict";
    var _context;
    var _x = this._x;
    var _fn0 = _x[0];
    var _result0 = _fn0(arg);
    if (_result0 !== undefined) {
        arg = _result0;
    }
    var _fn1 = _x[1];
    var _result1 = _fn1(arg);
    if (_result1 !== undefined) {
        arg = _result1;
    }
    return arg;
})
複製代碼

對於 SyncWaterfallHook,前面的 handler 返回值做爲下一個 handler 的輸入值,而且要求實例化 SyncWaterfallHook 的時候,傳入非零長度的數組。call 傳入的參數會做爲第一個 handler 的入參。

異步鉤子案例大全

全部的異步鉤子支持 tap、tapAsync、tapPromise 方法來註冊各類類型的 handler,可是不支持 call 方法來觸發 handler,只支持 promise、callAsync。

AsyncParallelBailHook(異步並行保險鉤子)

const apbh = new AsyncParallelBailHook()
apbh.tapAsync('1', (next) => {
  setTimeout(() => {
    next(1)
  }, 3000)
})
apbh.tapAsync('2', (next) => {
  setTimeout(() => {
    next(2)
  }, 1000)
})
apbh.callAsync((result) => {
  console.log(result)
  console.log('callback 執行完成')
})

// 打印以下
1 // 3s 後打印的
callback 執行完成
複製代碼

編譯的 fn 以下

(function anonymous(_callback) {
 "use strict";
    var _context;
    var _x = this._x;
    var _results = new Array(2);
    var _checkDone = () = >{
        for (var i = 0; i < _results.length; i++) {
            var item = _results[i];
            if (item === undefined) return false;
            if (item.result !== undefined) {
                _callback(null, item.result);
                return true;
            }
            if (item.error) {
                _callback(item.error);
                return true;
            }
        }
        return false;
    }
    do {
        var _counter = 2;
        var _done = () = >{
            _callback();
        };
        if (_counter <= 0) break;
        var _fn0 = _x[0];
        _fn0((_err0, _result0) = >{
            if (_err0) {
                if (_counter > 0) {
                    if (0 < _results.length && ((_results.length = 1), (_results[0] = {
                        error: _err0
                    }), _checkDone())) {
                        _counter = 0;
                    } else {
                        if (--_counter === 0) _done();
                    }
                }
            } else {
                if (_counter > 0) {
                    if (0 < _results.length && (_result0 !== undefined && (_results.length = 1), (_results[0] = {
                        result: _result0
                    }), _checkDone())) {
                        _counter = 0;
                    } else {
                        if (--_counter === 0) _done();
                    }
                }
            }
        });
        if (_counter <= 0) break;
        if (1 >= _results.length) {
            if (--_counter === 0) _done();
        } else {
            var _fn1 = _x[1];
            _fn1((_err1, _result1) = >{
                if (_err1) {
                    if (_counter > 0) {
                        if (1 < _results.length && ((_results.length = 2), (_results[1] = {
                            error: _err1
                        }), _checkDone())) {
                            _counter = 0;
                        } else {
                            if (--_counter === 0) _done();
                        }
                    }
                } else {
                    if (_counter > 0) {
                        if (1 < _results.length && (_result1 !== undefined && (_results.length = 2), (_results[1] = {
                            result: _result1
                        }), _checkDone())) {
                            _counter = 0;
                        } else {
                            if (--_counter === 0) _done();
                        }
                    }
                }
            });
        }
    } while ( false );
})
複製代碼

從 AsyncParallelBailHook 來看,每一個 handler 的最後一位形參是 next,它是一個函數,用戶必須手動執行而且傳參,這樣 callback 會拿到該參數而且執行。從例子能夠看出,callback 的執行是取決於註冊的 handler 的順序,雖然 next(2) 是在 1s 後就執行了,可是仍是不會觸發 callback,而是 next(1) 觸發了 callback。

AsyncParallelHook(異步並行鉤子)

const apl = new AsyncParallelHook()

apl.tapAsync('1', (next) => {
  setTimeout(() => {
    next(1)
  }, 3000)
})
apl.tapAsync('2', (next) => {
  setTimeout(() => {
    next(2)
  }, 1000)
})
apl.callAsync((result) => {
  console.log(result)
  console.log('callback 執行完成')
})

// 打印以下
2 // 1s 後打印的
callback 執行完成
複製代碼

編譯的 fn 以下

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

從 AsyncParallelHook 來看,每一個 handler 的最後一位形參是 next,它是一個函數,用戶必須手動執行而且傳參,這樣 callback 會拿到該參數而且執行。從例子能夠看出,callback 的執行是取決執行 next 函數的快慢。

AsyncSeriesBailHook(異步串行保險鉤子)

const asbh = new AsyncSeriesBailHook()

asbh.tapPromise('1', () => {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve(1)
    }, 3000)
  })
})
asbh.tapPromise('2', () => {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve(2)
    })
  })
})
asbh.promise().then((res) => {
  console.log(res)
})

// 打印以下
1 // 3s 後打印的
複製代碼

編譯的 fn 以下

(function anonymous() {
 "use strict";
    return new Promise((_resolve, _reject) = >{
        var _sync = true;
        var _context;
        var _x = this._x;
        var _fn0 = _x[0];
        var _hasResult0 = false;
        var _promise0 = _fn0();
        if (!_promise0 || !_promise0.then) throw new Error('Tap function (tapPromise) did not return promise (returned ' + _promise0 + ')');
        _promise0.then(_result0 = >{
            _hasResult0 = true;
            if (_result0 !== undefined) {
                _resolve(_result0);;
            } else {
                var _fn1 = _x[1];
                var _hasResult1 = false;
                var _promise1 = _fn1();
                if (!_promise1 || !_promise1.then) throw new Error('Tap function (tapPromise) did not return promise (returned ' + _promise1 + ')');
                _promise1.then(_result1 = >{
                    _hasResult1 = true;
                    if (_result1 !== undefined) {
                        _resolve(_result1);;
                    } else {
                        _resolve();
                    }
                },
                _err1 = >{
                    if (_hasResult1) throw _err1;
                    if (_sync) _resolve(Promise.resolve().then(() = >{
                        throw _err1;
                    }));
                    else _reject(_err1);
                });
            }
        },
        _err0 = >{
            if (_hasResult0) throw _err0;
            if (_sync) _resolve(Promise.resolve().then(() = >{
                throw _err0;
            }));
            else _reject(_err0);
        });
        _sync = false;
    });

})
複製代碼

咱們用 tapPromise 方法作了個測試,handler 必須返回一個 Promise,並且 AsyncSeriesBailHook 鉤子的 promise 方法返回的是一個 Promise,then 裏面的回調函數的參數與註冊的 handler 返回的 Promise 有關。

AsyncSeriesHook(異步串行鉤子)

const ash = new AsyncSeriesHook()

ash.tapAsync('1', (next) => {
  console.log(1)
  next()
})
ash.tapAsync('2', (next) => {
  console.log(2)
  next('觸發 callback')
})
ash.callAsync(function callback () {
  console.log('callback 執行完了')
})

// 打印以下
1
2
callback 執行完了
複製代碼

編譯的 fn 以下

(function anonymous(_callback) {
 "use strict";
    var _context;
    var _x = this._x;
    var _fn0 = _x[0];
    _fn0(_err0 = >{
        if (_err0) {
            _callback(_err0);
        } else {
            var _fn1 = _x[1];
            _fn1(_err1 = >{
                if (_err1) {
                    _callback(_err1);
                } else {
                    _callback();
                }
            });
        }
    });
})
複製代碼

串行執行 handler,handler 參數的最後一個是 next 函數,必須手動執行,纔會走到下面的邏輯。callback 的執行是根據 next是否傳參決定的。由以前的 tapbale-0.2.8源碼分析來看,以前爲了實現異步的鉤子,都須要函數內部有個遞歸調用的過程,如今編譯以後,全部的邏輯都扁平化了,不會引發遞歸佔用過多的空間的問題。這也是重構的好處。

AsyncSeriesWaterfallHook(異步串行瀑布鉤子)

const ash = new AsyncSeriesWaterfallHook(['name'])

ash.tapAsync('1', (name, next) => {
  console.log(name)
  next(null, '來自 handler 1 的參數')
})
ash.tapAsync('2', (name, next) => {
  console.log(name)
  next(null, '來自 handler 2 的參數')
})
ash.callAsync('來自初始化的參數', (err, name) => {
  console.log(name)
})

// 打印以下
來自初始化的參數
來自 handler 1 的參數
來自 handler 2 的參數
複製代碼

編譯的 fn 以下

(function anonymous(name, _callback) {
 "use strict";
    var _context;
    var _x = this._x;
    var _fn0 = _x[0];
    _fn0(name, (_err0, _result0) = >{
        if (_err0) {
            _callback(_err0);
        } else {
            if (_result0 !== undefined) {
                name = _result0;
            }
            var _fn1 = _x[1];
            _fn1(name, (_err1, _result1) = >{
                if (_err1) {
                    _callback(_err1);
                } else {
                    if (_result1 !== undefined) {
                        name = _result1;
                    }
                    _callback(null, name);
                }
            });
        }
    });
})
複製代碼

異步串行執行 handler,handler 參數的最後一個是 next 函數,必須手動執行,纔會走到下面的邏輯。callback 的執行是根據 next是否傳參決定的。第一個參數是 error,第二個參數是傳給下一個 handler 的值,若是 error 存在的話,直接會執行 callback。

同異步鉤子類的總結

分析了全部的同異步鉤子,根據以前的 tapable 版本,牽涉到異步執行的鉤子,函數內部確定是存在遞歸的,這樣寫起來容易讓人看懂。然而 2.0.0-beta 版本採用字符串拼接的方法把遞歸部分給抹平了,並且還會緩存每次編譯的生成的 fn。這樣來講,空間佔用就變少了,性能更好了。

Tapable

根據 tapbale-0.2.8 源碼分析,Tapable 是惟一的類。那麼 2.0.0-beta 版爲了兼容以前的語法,應該怎麼作呢。繼續定位到 Tapable.js

const util = require("util");
const SyncBailHook = require("./SyncBailHook");

function Tapable() {
    // 聲明同步保險鉤子
    this._pluginCompat = new SyncBailHook(["options"]);
    // 註冊 handler,主要是爲了將 pluginName camelize 化。
	this._pluginCompat.tap(
		{
			name: "Tapable camelCase",
			stage: 100
		},
		options => {
			options.names.add(
				options.name.replace(/[- ]([a-z])/g, (str, ch) => ch.toUpperCase())
			);
		}
    );
    // 在 hooks 屬性上對應的鉤子上註冊 handler
	this._pluginCompat.tap(
		{
			name: "Tapable this.hooks",
			stage: 200
		},
		options => {
			let hook;
			for (const name of options.names) {
				hook = this.hooks[name];
				if (hook !== undefined) {
					break;
				}
			}
			if (hook !== undefined) {
				const tapOpt = {
					name: options.fn.name || "unnamed compat plugin",
					stage: options.stage || 0
				};
				if (options.async) hook.tapAsync(tapOpt, options.fn);
				else hook.tap(tapOpt, options.fn);
				return true;
			}
		}
	);
}
module.exports = Tapable;

Tapable.addCompatLayer = function addCompatLayer(instance) {
	Tapable.call(instance);
	instance.plugin = Tapable.prototype.plugin;
	instance.apply = Tapable.prototype.apply;
};

// 註冊 handler,實際上會走到 _pluginCompat 屬性上的第二個 handler,進而在對應的 hooks 註冊了 handler。
Tapable.prototype.plugin = util.deprecate(function plugin(name, fn) {
	if (Array.isArray(name)) {
		name.forEach(function(name) {
			this.plugin(name, fn);
		}, this);
		return;
	}
	const result = this._pluginCompat.call({
		name: name,
		fn: fn,
		names: new Set([name])
	});
	if (!result) {
		throw new Error(
			`Plugin could not be registered at '${name}'. Hook was not found.\n` +
				"BREAKING CHANGE: There need to exist a hook at 'this.hooks'. " +
				"To create a compatibility layer for this hook, hook into 'this._pluginCompat'."
		);
	}
}, "Tapable.plugin is deprecated. Use new API on `.hooks` instead");

Tapable.prototype.apply = util.deprecate(function apply() {
	for (var i = 0; i < arguments.length; i++) {
		arguments[i].apply(this);
	}
}, "Tapable.apply is deprecated. Call apply on the plugin directly instead");
複製代碼

Tapable 重構以後,爲了兼容以前的版本,費了一點心思,首先 Tapable 上有個 _pluginCompat 屬性是同步保險鉤子,而且註冊了兩個 handler,這兩個 handler 的觸發時機是在於你調用 plugin 方法的時候,先將你傳入的插件名 camelize 化,而後在 hooks 屬性上尋找對應的鉤子實例,而且調用 tap 方法真正註冊 handler。

這麼作的目的在於什麼呢?由於 webpack 的 Compiler 類就是繼承於 Tapable,因此 webpack 與 Tapable 升級了,因爲內部作了必定的兼容,不會對用戶之前的 plugin 形成任何影響。因此用戶不用再重寫他們的 plugin 了。對於 webpack 開發插件,只須要提供帶有 apply 方法的對象或者提供一個函數,插件在鉤子實例上註冊 handler 的時候,依然能夠經過 compiler.plugin 來註冊插件,可是命令行會打印出提示語句,提示你儘可能使用新語法,能夠看出插件升級以後的影響也是降到最低。

所獲

通過分析了 tapable-0.2.8 以及 tapable-2.0.0-beta 版本的源碼,深入地體會到做者 js 的功底之深厚,前一個版本對於 js 基礎好一點的人都能寫出來,可是後一個版本的總體架構設計,以及對前一個版本的兼容都是作的很是好的。以前看了 javascript 設計模式什麼的,如今都以爲都是泛泛之談,而真正能應用於實際場景才說明你對各類設計模式融會貫通,不是爲了追求設計模式,在無形當中,你的感受會帶着你走,會寫出高質量的代碼。這也是大量閱讀優秀源碼的好處。好比 Vue、Vuex、Vue-Router 的架構設計,以及這篇Vue 全家桶源碼解析

相關文章
相關標籤/搜索