深刻源碼解析 tapable 實現原理

引子

若是你瞭解過 webpack,他們會告訴你,webpack 底層是基於 tapablejavascript

若是你好奇 tapable 是什麼,你可能會看到其餘地方的博客:『Tapble是webpack在打包過程當中,控制打包在什麼階段調用Plugin的庫,是一個典型的觀察者模式的實現』。java

可能,他們還會告訴你,Tapable的核心功能就是控制一系列註冊事件之間的執行流控制,對吧?webpack

若是你瞭解的繼續深一些,可能還會看到下面的表格,見到如此多的鉤子:web

名稱 鉤入的方式 做用
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 屬性

那麼,問題來了,這些鉤子的內部是如何實現的?它們之間有什麼樣的繼承關係? 源碼設計上有什麼優化地方?api

本文接下來,將從 tapable 源碼出發,解開 tapable 神祕的面紗。數組

Tapable 源碼核心

先上一張大圖,涵蓋了 80% 的 tapable 核心流程promise

上圖中,咱們看到, tapable 這個框架,最底層的有兩個類: 基礎類 Hook, 工廠類 HookCodeFactory緩存

上面列表中 tapable 提供的鉤子,好比說 SyncHookSyncWaterHooks等,都是繼承自基礎類 Hook閉包

圖中可見,這些鉤子,有兩個最關鍵的方法: tap方法、 call 方法。併發

這兩個方法是tapable 暴露給用戶的api, 簡單且好用。 webpack 是基於這兩個api 建構出來的一套複雜的工做流。

咱們再來看工廠類 HookCodeFactory,它也衍生出SyncHookCodeFactorySyncWaterCodeFactory 等不一樣的工廠構造函數,實例化出來不一樣工廠實例factory

工廠實例factory的做用是,拼接生產出不一樣的 compile 函數,生產 compile 函數的過程,本質上就是拼接字符串,沒有什麼魔法,下文中會介紹到。

這些不一樣的 compile 函數,最終會在 call() 方法被調用。

呼,剛纔介紹了一大堆概念,但願沒有把讀者弄暈

咱們首先看一下,call 方法和 tap 方法是如何使用的。

基本用法

下面是簡單的一個例子:

let hook = new SyncHook(['foo']);

hook.tap({
    name: 'dudu',
    before: '',
}, (params) => {
    console.log('11')
})

hook.tap({
    name: 'lala',
    before: 'dudu',
}, (params) => {
    console.log('22')
})

hook.tap({
 name: 'xixi',
 stage: -1
}, (params) => {
 console.log('22')
})

hook.call('tapable', 'learn')
複製代碼

上面代碼的輸出結果:

// 22
// 11
複製代碼

咱們使用 tap()方法用於註冊事件,使用 call() 來觸發全部回調函數執行。

注意點:

  • 在實例化 SyncHook 時,咱們傳入字符串數組。數組的長度很重要,會影響你經過 call 方法調用 handler 時入參個數。就像例子所示,調用 call 方法傳入的是兩個參數,實際上 handler 只能接收到一個參數,由於你在new SyncHook 的時候傳入的字符串數組長度是1。

  • 經過 tap 方法去註冊 handler 時,第一個參數必須有,格式以下:

    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 就能共享這個對象
    }
    複製代碼

    上面參數,咱們重點關注 beforestage,這兩個參數影響了回調函數的執行順序 。上文例子中, name'lala'handler 註冊的時候,是傳了一個對象,它的 before 屬性爲 dudu,說明這個 handler 要插到 nameduduhandler 以前執行。可是又由於 namexixihandler 註冊的時候,stage 屬性爲 -1,比其餘的 handlerstage 要小,因此它會被移到最前面執行。

那麼,tapcall是如何實現的呢? 被調用的時候,背後發生了什麼?

咱們接下來,深刻到源碼分析 tapable 機制。

下文中分析的源碼是 tapable v1.1.3 版本

tap 方法的實現

上文中,咱們在註冊事件時候,用了 hook.tap() 方法。

tap 方法核心是,把註冊的回調函數,維護在這個鉤子的一個數組中。

tap 方法實如今哪裏呢?

代碼裏面,hookSyncHook 的實例,SyncHook又繼承了 Hook 基類,在 Hook 基類中,具體代碼以下:

class Hook {
    tap(options, fn) {
        options = this._runRegisterInterceptors(options);
        this._insert(options);
    }
}
複製代碼

咱們發現,tap 方法最終調用了_insert方法,

_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);
        }
        // 默認 stage是0
        // stage 值越大,
        let stage = 0;
        if (typeof item.stage === "number") stage = item.stage;
        let i = this.taps.length;
        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;
    }
複製代碼

把註冊的方法,都 push 到一個 taps 數組上面。這裏對 beforestage 作了處理,使得 push 到 taps 數組的順序不一樣,從而決定了 回調函數的執行順序不一樣。

call 方法的實現

SyncHook.js 中,咱們沒有找到 call 方法的定義。再去 Hook 基類上找,發現有這樣一句, call 方法 是 _call 方法

this.call = this._call;
複製代碼
class Hook {
    construcotr {
        // 這裏發現,call 方法就是 this._call 方法
        this.call = this._call;
    }
    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
        });
    }
}
複製代碼

那麼, _call 方法是在哪裏定義的呢?看下面, this._callcreateCompileDelegate("call", "sync")的返回值。

Object.defineProperties(Hook.prototype, {
    // this._call 是 createCompileDelegate("call", "sync") 的值, 爲函數 lazyCompileHook
    _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 是閉包保存的字符串 'call'
function createCompileDelegate(name, type) {
    return function lazyCompileHook(...args) {
        // 實際上
        // this.call = this._creteCall(type)
        // return this.call()
        this[name] = this._createCall(type);
        return this[name](...args);
    };
}
複製代碼

上面的代碼,createCompileDelegate 先調用 this._createCall() 方法,把返回值賦值給 this[name]

this._createCall() 裏面本質是調用了this.compiler 方法,可是基類Hook上的compiler() 方法是一個空實現,順着這條線索找下來,這是一條死衚衕。

this.compiler 方法,真正是定義在衍生類 SyncHook上,也就是在 SyncHook.js 中,SyncHook 類從新定義了 compiler 方法來覆蓋:

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

這裏的 factory ,就是本文開頭提到的工廠實例。factory.create 的產物以下:

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

this._x 是一個數組,裏面存放的就是咱們註冊的 taps 方法。上面代碼的核心就是,遍歷咱們註冊的 taps 方法,並去執行。

factory.create 的核心是,根據傳入的type 類型,拼接對應的字符串,代碼以下:

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

上面代碼中, content 方法是定義在 SyncHook 的衍生類上的,

class SyncHookCodeFactory extends HookCodeFactory {
    // 區分不一樣的類型的 工程
    // content 方法用於拼接字符串
    // HookCodeFactory 裏面會調用 this.content(), 訪問到的是這裏的 content
    content({ onError, onDone, rethrowIfPossible }) {
        return this.callTapsSeries({
            onError: (i, err) => onError(err),
            onDone,
            rethrowIfPossible
        });
    }
}
複製代碼

到這裏爲止一目瞭然,咱們能夠看到咱們的註冊回調是怎樣在this.call方法中一步步執行的。

在這裏的優化, tapable 用到了《javascript 高級程序設計》中的『惰性函數』,緩存下來 this.__createCall call,從而提高性能

惰性函數

什麼是惰性函數? 惰性函數有什麼做用?

基類Hook上的compiler 方法是一個空實現,具體實現是 衍生類 上

compile 傳入的參數很豐富

return this.compile({
 taps: this.taps,
 interceptors: this.interceptors,
 args: this._args,
 type: type
 });
複製代碼

工廠的產物

Tapable有一系列Hook方法,可是這麼多的Hook方法都是無非是爲了控制註冊事件的執行順序以及異常處理

最簡單的SyncHook前面已經講過,咱們從SyncBailHook開始看。

SyncBailHook

這類鉤子的特色是,判斷 handler 的返回值,是否===undefined, 若是是 undefined , 則執行,若是有返回值,則 return 返回值

// fn, 調用 call 時,實際執行的代碼
function anonymous(/*``*/) {
 "use strict";
	var _context;
	var _x = this._x;
	var _fn0 = _x[0];
	var _result0 = _fn0();
	if (_result0 !== undefined) {
		return _result0;
	} else {
		var _fn1 = _x[1];
		var _result1 = _fn1();
		if (_result1 !== undefined) {
			return _result1;
		} else {
		}
	}
}
複製代碼

經過打印fn,咱們能夠輕易的看出,SyncBailHook提供了停止註冊函數執行的機制,只要在某個註冊回調中返回一個非undefined的值,運行就會停止。

SyncWaterfallHook

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

能夠看出SyncWaterfallHook就是將上一個事件註冊回調的返回值做爲下一個註冊函數的參數,這就要求在new SyncWaterfallHook(['arg1']);須要且只能傳入一個形參。

SyncLoopHook

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

SyncLoopHook只有當上一個註冊事件函數返回undefined的時候纔會執行下一個註冊函數,不然就不斷重複調用。

AsyncSeriesHook

Series有順序的意思,這個Hook用於按順序執行異步函數。

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

從打印結果能夠發現,兩個事件以前是串行的,而且next中能夠傳入err參數,當傳入err,直接中斷異步,而且將err傳入咱們在call方法傳入的完成回調函數中。

AsyncParallelHook

asyncParallelHook 是異步併發的鉤子,適用場景:一些狀況下,咱們去併發的請求不相關的接口,好比說請求用戶的頭像接口、地址接口。

factory.create 的產物是下面的字符串

function anonymous(_callback) {
 "use strict";
    var _context;
    var _x = this._x;
    do {
        // _counter 是 註冊事件的數量
        var _counter = 2;
        var _done = () => {
            _callback();
        };

        if (_counter <= 0) break;

        var _fn0 = _x[0];

        _fn0(_err0 => {
            // 這個函數是 next 函數
            // 調用這個函數的時間不能肯定,有可能已經執行了接下來的幾個註冊函數
            if (_err0) {
                // 若是還沒執行全部註冊函數,終止
                if (_counter > 0) {
                    _callback(_err0);
                    _counter = 0;
                }
            } else {
                // 檢查 _counter 的值,若是是 0 的話,則結束
                // 一樣,因爲函數實際調用時間沒法肯定,須要檢查是否已經運行完畢,
                if (--_counter === 0) {
                    _done()
                };
            }
        });

        // 執行下一個註冊回調以前,檢查_counter是否被重置等,若是重置說明某些地方返回err,直接終止。
        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);
}
複製代碼

從打印結果看出Event2的調用在AsyncCall in Event1以前,說明異步事件是併發的。

相關文章
相關標籤/搜索