引子
如果你瞭解過 webpack
,他們會告訴你,webpack
底層是基於 tapable
。
如果你好奇 tapable
是什麼,你可能會看到其他地方的部落格:『Tapble
是webpack在打包過程中,控制打包在什麼階段呼叫Plugin的庫,是一個典型的觀察者模式的實現』。
可能,他們還會告訴你,Tapable的核心功能就是控制一系列註冊事件之間的執行流控制,對吧?
如果你瞭解的繼續深一些,可能還會看到下面的表格,見到如此多的鉤子:
名稱 | 鉤入的方式 | 作用 |
---|---|---|
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 |
Hook Helper 與 Tapable 類
名稱 | 作用 |
---|---|
HookCodeFactory | 編譯生成可執行 fn 的工廠類 |
HookMap | Map 結構,儲存多個 Hook 例項 |
MultiHook | 組合多個 Hook 例項 |
Tapable | 向前相容老版本,例項必須擁有 hooks 屬性 |
那麼,問題來了,這些鉤子的內部是如何實現的?它們之間有什麼樣的繼承關係? 原始碼設計上有什麼優化地方?
本文接下來,將從 tapable 原始碼出發,解開 tapable 神祕的面紗。
Tapable 原始碼核心
先上一張大圖,涵蓋了 80% 的 tapable 核心流程
上圖中,我們看到, 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 版本
tap 方法的實現
上文中,我們在註冊事件時候,用了 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 陣列的順序不同,從而決定了 回撥函式的執行順序不同。
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._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
,從而提升效能
惰性函式
什麼是惰性函式? 惰性函式有什麼作用?
比如說,我們定義一個函式,我們需要在這個函式裡面判斷不同的瀏覽器環境,走不同的邏輯。
function addEvent (type, element, fun) {
if (element.addEventListener) {
element.addEventListener(type, fun, false);
} else if(element.attachEvent){
element.attachEvent('on' + type, fun);
} else{
element['on' + type] = fun;
}
}
複製程式碼
上面 addEvent
方法,每執行一遍,都需要判斷一次,有沒有什麼優化方法呢?
答案就是惰性函式。
function addEvent (type, element, fun) {
if (element.addEventListener) {
addEvent = function (type, element, fun) {
element.addEventListener(type, fun, false);
}
} else if(element.attachEvent){
addEvent = function (type, element, fun) {
element.attachEvent('on' + type, fun);
}
} else{
addEvent = function (type, element, fun) {
element['on' + type] = fun;
}
}
return addEvent(type, element, fun);
}
複製程式碼
上面的惰性函式,只會在第一次執行的時候判斷環境,之後每次執行,其實是執行被重新賦值的 addEvent
方法,判斷的結果被快取了下來。
tapable
裡用到惰性函式的地方,大概可以簡化成如下:
this._call = function(){
this.call = this._creteCall(type)
return this.call()
}
複製程式碼
this._call
只有在第一次執行的時候,才會拼接生產出字串方法,之後再執行時,就會執行被快取下來的字串方法,從而優化了效能。
factory工廠的產物
Tapable有一系列Hook方法,但是這麼多的Hook方法都是無非是為了控制註冊事件的執行順序以及異常處理。
最簡單的SyncHook
的factory 的工廠產物,前面已經講過,我們從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之前,說明非同步事件是併發的。
位元組跳動大大大大量量量量招人了
位元組跳動(杭州|北京|上海)大量招人,福利超級棒,薪資水平秒殺 BAT,上班不打卡、每天下午茶、免費零食無限供應、免費三餐(我念下選單,大閘蟹鮑魚扇貝海鮮烤魚片黑椒牛柳咖哩牛肉麻辣小龍蝦)、免費健身房、入職配touch bar15寸頂配全新mbp、每月還有租房房補。 這次真的機會多多,年後研發人數要擴招n倍,技術氛圍好,大牛多,加班少,還猶豫什麼?快發簡歷到下方郵箱,就現在!
僅僅是一小部分的jd連結如下, 更多的歡迎加微信~
前端jd: job.toutiao.com/s/bJM4Anjob…
後端jd: job.toutiao.com/s/bJjjTsjob…
測試jd: job.toutiao.com/s/bJFv9bjob…
產品jd: job.toutiao.com/s/bJBgV8job…
前端實習生: job.toutiao.com/s/bJ6NjAjob…
後端實習生: job.toutiao.com/s/bJrjrkjob…
持續招聘大量前端、服務端、客戶端、測試、產品,實習社招都闊以
簡歷發 dujuncheng@bytedance.com,建議加微信 dujuncheng1,可以聊天聊地聊人生,請註明來自掘金以及要投遞哪裡的崗位