若是你瞭解過 webpack
,他們會告訴你,webpack
底層是基於 tapable
。javascript
若是你好奇 tapable
是什麼,你可能會看到其餘地方的博客:『Tapble
是webpack在打包過程當中,控制打包在什麼階段調用Plugin的庫,是一個典型的觀察者模式的實現』。java
可能,他們還會告訴你,Tapable的核心功能就是控制一系列註冊事件之間的執行流控制,對吧?webpack
若是你瞭解的繼續深一些,可能還會看到下面的表格,見到如此多的鉤子:web
名稱 | 鉤入的方式 | 做用 |
---|---|---|
Hook | tap , tapAsync ,tapPromise |
鉤子基類 |
SyncHook | tap |
同步鉤子 |
SyncBailHook | tap |
同步鉤子,只要執行的 handler 有返回值,剩餘 handler 不執行 |
SyncLoopHook | tap |
同步鉤子,只要執行的 handler 有返回值,一直循環執行此 handler |
SyncWaterfallHook | tap |
同步鉤子,上一個 handler 的返回值做爲下一個 handler 的輸入值 |
AsyncParallelBailHook | tap , tapAsync ,tapPromise |
異步鉤子,handler 並行觸發,可是跟 handler 內部調用回調函數的邏輯有關 |
AsyncParallelHook | tap , tapAsync ,tapPromise |
異步鉤子,handler 並行觸發 |
AsyncSeriesBailHook | tap , tapAsync ,tapPromise |
異步鉤子,handler 串行觸發,可是跟 handler 內部調用回調函數的邏輯有關 |
AsyncSeriesHook | tap , tapAsync ,tapPromise |
異步鉤子,handler 串行觸發 |
AsyncSeriesLoopHook | tap , tapAsync ,tapPromise |
異步鉤子,能夠觸發 handler 循環調用 |
AsyncSeriesWaterfallHook | tap , tapAsync ,tapPromise |
異步鉤子,上一個 handler 能夠根據內部的回調函數傳值給下一個 handler |
名稱 | 做用 |
---|---|
HookCodeFactory | 編譯生成可執行 fn 的工廠類 |
HookMap | Map 結構,存儲多個 Hook 實例 |
MultiHook | 組合多個 Hook 實例 |
Tapable | 向前兼容老版本,實例必須擁有 hooks 屬性 |
那麼,問題來了,這些鉤子的內部是如何實現的?它們之間有什麼樣的繼承關係? 源碼設計上有什麼優化地方?api
本文接下來,將從 tapable 源碼出發,解開 tapable 神祕的面紗。數組
先上一張大圖,涵蓋了 80% 的 tapable 核心流程promise
上圖中,咱們看到, tapable
這個框架,最底層的有兩個類: 基礎類 Hook
, 工廠類 HookCodeFactory
。緩存
上面列表中 tapable
提供的鉤子,好比說 SyncHook
、 SyncWaterHooks
等,都是繼承自基礎類 Hook
。閉包
圖中可見,這些鉤子,有兩個最關鍵的方法: tap
方法、 call
方法。併發
這兩個方法是tapable
暴露給用戶的api
, 簡單且好用。 webpack
是基於這兩個api 建構出來的一套複雜的工做流。
咱們再來看工廠類 HookCodeFactory
,它也衍生出SyncHookCodeFactory
、 SyncWaterCodeFactory
等不一樣的工廠構造函數,實例化出來不一樣工廠實例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 就能共享這個對象
}
複製代碼
上面參數,咱們重點關注 before
和 stage
,這兩個參數影響了回調函數的執行順序 。上文例子中, name
爲 'lala'
的 handler
註冊的時候,是傳了一個對象,它的 before
屬性爲 dudu
,說明這個 handler
要插到 name
爲 dudu
的 handler
以前執行。可是又由於 name
爲 xixi
的 handler
註冊的時候,stage
屬性爲 -1
,比其餘的 handler
的 stage
要小,因此它會被移到最前面執行。
那麼,tap
和 call
是如何實現的呢? 被調用的時候,背後發生了什麼?
咱們接下來,深刻到源碼分析 tapable
機制。
下文中分析的源碼是 tapable v1.1.3 版本
上文中,咱們在註冊事件時候,用了 hook.tap()
方法。
tap
方法核心是,把註冊的回調函數,維護在這個鉤子的一個數組中。
tap
方法實如今哪裏呢?
代碼裏面,hook
是 SyncHook
的實例,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
數組上面。這裏對 before
和 stage
作了處理,使得 push
到 taps 數組的順序不一樣,從而決定了 回調函數的執行順序不一樣。
在 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._call
是 createCompileDelegate("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
開始看。
這類鉤子的特色是,判斷 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
的值,運行就會停止。
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']);
須要且只能傳入一個形參。
// 打印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的時候纔會執行下一個註冊函數,不然就不斷重複調用。
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
是異步併發的鉤子,適用場景:一些狀況下,咱們去併發的請求不相關的接口,好比說請求用戶的頭像接口、地址接口。
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以前,說明異步事件是併發的。