webpack系列之二Tapable

滴滴WebApp架構組發表於1970-01-01

作者:崔靜

上一篇總覽 我們介紹了 webpack 整體的編譯過程,這次就來分析下基礎的 Tapable。

介紹

webpack 整個編譯過程中暴露出來大量的 Hook 供內部/外部外掛使用,同時支援擴充套件各種外掛,而內部處理的程式碼,也依賴於 Hook 和外掛,這部分的功能就依賴於 Tapable。webpack 的整體執行過程,總的來看就是事件驅動的。從一個事件,走向下一個事件。Tapable 用來提供各種型別的 Hook。我們通過下面一個直觀的使用例子,初步認識一下 Tapable:

const { 
SyncHook
} = require('tapable')// 建立一個同步 Hook,指定引數const hook = new SyncHook(['arg1', 'arg2'])// 註冊hook.tap('a', function (arg1, arg2) {
console.log('a')
})hook.tap('b', function (arg1, arg2) {
console.log('b')
})hook.call(1, 2)複製程式碼

看起來起來功能和 EventEmit 類似,先註冊事件,然後觸發事件。不過 Tapable 的功能要比 EventEmit 強大。從官方介紹中,可以看到 Tapable 提供了很多型別的 Hook,分為同步和非同步兩個大類(非同步中又區分非同步並行和非同步序列),而根據事件執行的終止條件的不同,由衍生出 Bail/Waterfall/Loop 型別。

下圖展示了每種型別的作用:

Basic &<br /> Bail” class=”lazyload” data-height=”1246″ src=”https://user-gold-cdn.xitu.io/2018/12/28/167f458ac2b1e527?imageView2/0/w/1280/h/960/ignore-error/1″ data-width=”1280″><figcaption></figcaption></figure>
</p>
<figure><img alt= Loop” class=”lazyload” data-height=”1050″ src=”https://user-gold-cdn.xitu.io/2018/12/28/167f458d6ff8424f?imageView2/0/w/1280/h/960/ignore-error/1″ data-width=”1280″>
  • BasicHook: 執行每一個,不關心函式的返回值,有 SyncHook、AsyncParallelHook、AsyncSeriesHook。

    我們平常使用的 eventEmit 型別中,這種型別的鉤子是很常見的。

  • BailHook: 順序執行 Hook,遇到第一個結果 result !== undefined 則返回,不再繼續執行。有:SyncBailHook、AsyncSeriseBailHook, AsyncParallelBailHook。

    什麼樣的場景下會使用到 BailHook 呢?設想如下一個例子:假設我們有一個模組 M,如果它滿足 A 或者 B 或者 C 三者任何一個條件,就將其打包為一個單獨的。這裡的 A、B、C 不存在先後順序,那麼就可以使用 AsyncParallelBailHook 來解決:

     x.hooks.拆分模組的Hook.tap('A', () =>
    {
    if (A 判斷條件滿足) {
    return true
    }
    }) x.hooks.拆分模組的Hook.tap('B', () =>
    {
    if (B 判斷條件滿足) {
    return true
    }
    }) x.hooks.拆分模組的Hook.tap('C', () =>
    {
    if (C 判斷條件滿足) {
    return true
    }
    })複製程式碼

    如果 A 中返回為 true,那麼就無須再去判斷 B 和 C。但是當 A、B、C 的校驗,需要嚴格遵循先後順序時,就需要使用有順序的 SyncBailHook(A、B、C 是同步函式時使用) 或者 AsyncSeriseBailHook(A、B、C 是非同步函式時使用)。

  • WaterfallHook: 類似於 reduce,如果前一個 Hook 函式的結果 result !== undefined,則 result 會作為後一個 Hook 函式的第一個引數。既然是順序執行,那麼就只有 Sync 和 AsyncSeries 類中提供這個Hook:SyncWaterfallHook,AsyncSeriesWaterfallHook

    當一個資料,需要經過 A,B,C 三個階段的處理得到最終結果,並且 A 中如果滿足條件 a 就處理,否則不處理,B 和 C 同樣,那麼可以使用如下

    x.hooks.tap('A', (data) =>
    {
    if (滿足 A 需要處理的條件) {
    // 處理資料 data return data
    } else {
    return
    }
    })x.hooks.tap('B', (data) =>
    {
    if (滿足B需要處理的條件) {
    // 處理資料 data return data
    } else {
    return
    }
    }) x.hooks.tap('C', (data) =>
    {
    if (滿足 C 需要處理的條件) {
    // 處理資料 data return data
    } else {
    return
    }
    })複製程式碼
  • LoopHook: 不停的迴圈執行 Hook,直到所有函式結果 result === undefined。同樣的,由於對序列性有依賴,所以只有 SyncLoopHook 和 AsyncSeriseLoopHook (PS:暫時沒看到具體使用 Case)

原理

我們先給出 Tapable 程式碼的主脈絡:

hook 事件註冊 ——>
hook 觸發 ——>
生成 hook 執行程式碼 ——>
執行

hook 類關係圖很簡單,各種 hook 都繼承自一個基本的 Hook 抽象類,同時內部包含了一個 xxxCodeFactory 類,會在生成 hook 執行程式碼中用到。

tapable uml

事件註冊

Tapable 基本邏輯是,先通過類例項的 tap 方法註冊對應 Hook 的處理函式:

事件註冊

Tapable 提供了 tap/tapAsync/tapPromise 這三個註冊事件的方法(實現邏輯在 Hook 基類中),分別針對同步(tap)/非同步(tapAsync/tapPromise),對要 push 到 taps 中的內容賦給不一樣的 type 值,如上圖所示。

對於 SyncHook, SyncBailHook, SyncLoopHook, SyncWaterfallHook 這四個同步型別的 Hook, 則會覆寫基類中 tapAsync 和 tapPromise 方法,防止使用者在同步 Hook 中誤用非同步方法。

 tapAsync() { 
throw new Error("tapAsync is not supported on a SyncHook");

} tapPromise() {
throw new Error("tapPromise is not supported on a SyncHook");

}複製程式碼

事件觸發

與 tap/tapAsync/tapPromise 相對應的,Tapable 中提供了三種觸發事件的方法 call/callAsync/promise。這三這方法也位於基類 Hook 中,具體邏輯如下

this.call = this._call = this._createCompileDelegate("call", "sync");
this.promise = this._promise = this._createCompileDelegate("promise", "promise");
this.callAsync = this._callAsync = this._createCompileDelegate("callAsync", "async");
// ..._createCall(type) {
return this.compile({
taps: this.taps, interceptors: this.interceptors, args: this._args, type: type
});

}_createCompileDelegate(name, type) {
const lazyCompileHook = (...args) =>
{
this[name] = this._createCall(type);
return this[name](...args);

};
return lazyCompileHook;

}複製程式碼

無論是 call, 還是 callAsync 和 promise,最終都會呼叫到 compile 方法,再此之前,其區別就是 compile 中所傳入的 type 值的不同。而 compile 根據不同的 type 型別生成了一個可執行函式,然後執行該函式。

注意上面程式碼中有一個變數名稱 lazyCompileHook,懶編譯。當我們 new Hook 的時候,其實會先生成了 promise, call, callAsync 對應的 CompileDelegate 程式碼,其實際的結構是

this.call = (...args) =>
{
this[name] = this._createCall('sync');
return this['call'](...args);

}this.promise = (...args) =>
{
this[name] = this._createCall('promise');
return this['promise'](...args);

}this.callAsync = (...args) =>
{
this[name] = this._createCall('async');
return this['callAsync'](...args);

}複製程式碼

當在觸發 hook 時,比如執行 xxhook.call() 時,才會編譯出對應的執行函式。這個過程就是所謂的“懶編譯”,即用的時候才編譯,已達到最優的執行效率。

接下來我們主要看 compile 的邏輯,這塊也是 Tapable 中大部分的邏輯所在。

執行程式碼生成

在看原始碼之前,我們可以先寫幾個簡單的 demo,看一下 Tapable 最終生成了什麼樣的執行程式碼,來直觀感受一下:

執行的程式碼

上圖分別是 SyncHook.call, AsyncSeriesHook.callAsync 和 AsyncSeriesHook.promise 生成的程式碼。_x 中儲存了註冊的事件函式,_fn${index
}
則是每一個函式的執行,而生成的程式碼中根據不同的 Hook 以及以不同的呼叫方式, _fn${index
}
會有不同的執行方式。這些差異是如何通過程式碼生成的呢?我們來細看 compile 方法。

compile 這個方法在基類中並沒有實現,其實現位於派生出來的各個類中。以 SyncHook 為例,看一下

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 {
// ... 省略其他程式碼 compile(options) {
factory.setup(this, options);
return factory.create(options);

}
}複製程式碼

這裡生成可執行程式碼使用了工廠模式:HookCodeFactory 是一個用來生成程式碼的工廠基類,每一個 Hook 中派生出一個子類。所有的 Hook 中 compile 都呼叫到了 create 方法。先來看一下這個 create 方法做了什麼。

create(options) { 
this.init(options);
switch(this.options.type) {
case "sync": return new Function(this.args(), "\"use strict\";
\n"
+ this.header() + this.content({
onError: err =>
`throw ${err
}
;
\n`
, onResult: result =>
`return ${result
}
;
\n`
, onDone: () =>
"", rethrowIfPossible: true
}));
case "async": return 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"

}));
case "promise": let code = "";
code += "\"use strict\";
\n"
;
code += "return new Promise((_resolve, _reject) =>
{\n"
;
code += "var _sync = true;
\n"
;
code += this.header();
code += this.content({
onError: err =>
{
let code = "";
code += "if(_sync)\n";
code += `_resolve(Promise.resolve().then(() =>
{
throw ${err
}
;

}));
\n`
;
code += "else\n";
code += `_reject(${err
}
);
\n`
;
return code;

}, onResult: result =>
`_resolve(${result
}
);
\n`
, onDone: () =>
"_resolve();
\n"

});
code += "_sync = false;
\n"
;
code += "
});
\n"
;
return new Function(this.args(), code);

}
}複製程式碼

乍一看程式碼有點多,簡化一下,畫個圖,就是下面的流程:

執行流程

由此可以看到,create 中只實現了程式碼的主模板,實現了公共的部分(函式引數和函式一開始的公共引數),然後留出差異的部分 content,交給各個子類來實現。然後橫向對比一下各個 Hook 中繼承自 HookCodeFactory 的子 CodeFactory,看一下 content 的實現差異:

//syncHookclass SyncHookCodeFactory extends HookCodeFactory { 
content({
onError, onResult, onDone, rethrowIfPossible
}) {
return this.callTapsSeries({
onError: (i, err) =>
onError(err), onDone, rethrowIfPossible
});

}
}//syncBailHookcontent({
onError, onResult, onDone, rethrowIfPossible
}) {
return this.callTapsSeries({
onError: (i, err) =>
onError(err), onResult: (i, result, next) =>
`if(${result
}
!== undefined) {\n${onResult(result)
}
;
\n
} else {\n${next()
}

}\n`
, onDone, rethrowIfPossible
});

}//AsyncSeriesLoopHookclass AsyncSeriesLoopHookCodeFactory extends HookCodeFactory {
content({
onError, onDone
}) {
return this.callTapsLooping({
onError: (i, err, next, doneBreak) =>
onError(err) + doneBreak(true), onDone
});

}
}// 其他的結構都類似,便不在這裡貼程式碼了複製程式碼

可以看到,在所有的子類中,都實現了 content 方法,根據不同鉤子執行流程的不同,呼叫了 callTapsSeries/callTapsParallel/callTapsLooping 並且會有 onError, onResult, onDone, rethrowIfPossible 這四中情況下的程式碼片段。

callTapsSeries/callTapsParallel/callTapsLooping 都在基類的方法中,這三個方法中都會走到一個 callTap 的方法。先看一下 callTap 方法。程式碼比較長,不想看程式碼的可以直接看後面的圖。

callTap(tapIndex, { 
onError, onResult, onDone, rethrowIfPossible
}) {
let code = "";
let hasTapCached = false;
// 這裡的 interceptors 先忽略 for(let i = 0;
i <
this.options.interceptors.length;
i++) {
const interceptor = this.options.interceptors[i];
if(interceptor.tap) {
if(!hasTapCached) {
code += `var _tap${tapIndex
}
= ${this.getTap(tapIndex)
}
;
\n`
;
hasTapCached = true;

} code += `${this.getInterceptor(i)
}
.tap(${interceptor.context ? "_context, " : ""
}
_tap${tapIndex
}
);
\n`
;

}
} code += `var _fn${tapIndex
}
= ${this.getTapFn(tapIndex)
}
;
\n`
;
const tap = this.options.taps[tapIndex];
switch(tap.type) {
case "sync": if(!rethrowIfPossible) {
code += `var _hasError${tapIndex
}
= false;
\n`
;
code += "try {\n";

} if(onResult) {
code += `var _result${tapIndex
}
= _fn${tapIndex
}
(${this.args({
before: tap.context ? "_context" : undefined
}
)
});
\n`
;

} else {
code += `_fn${tapIndex
}
(${this.args({
before: tap.context ? "_context" : undefined
}
)
});
\n`
;

} if(!rethrowIfPossible) {
code += "
} catch(_err) {\n"
;
code += `_hasError${tapIndex
}
= true;
\n`
;
code += onError("_err");
code += "
}\n"
;
code += `if(!_hasError${tapIndex
}
) {\n`
;

} if(onResult) {
code += onResult(`_result${tapIndex
}
`
);

} if(onDone) {
code += onDone();

} if(!rethrowIfPossible) {
code += "
}\n"
;

} break;
case "async": let cbCode = "";
if(onResult) cbCode += `(_err${tapIndex
}
, _result${tapIndex
}
) =>
{\n`
;
else cbCode += `_err${tapIndex
}
=>
{\n`
;
cbCode += `if(_err${tapIndex
}
) {\n`
;
cbCode += onError(`_err${tapIndex
}
`
);
cbCode += "
} else {\n"
;
if(onResult) {
cbCode += onResult(`_result${tapIndex
}
`
);

} if(onDone) {
cbCode += onDone();

} cbCode += "
}\n"
;
cbCode += "
}"
;
code += `_fn${tapIndex
}
(${this.args({
before: tap.context ? "_context" : undefined, after: cbCode
}
)
});
\n`
;
break;
case "promise": code += `var _hasResult${tapIndex
}
= false;
\n`
;
code += `_fn${tapIndex
}
(${this.args({
before: tap.context ? "_context" : undefined
}
)
}).then(_result${tapIndex
}
=>
{\n`
;
code += `_hasResult${tapIndex
}
= true;
\n`
;
if(onResult) {
code += onResult(`_result${tapIndex
}
`
);

} if(onDone) {
code += onDone();

} code += `
}, _err${tapIndex
}
=>
{\n`
;
code += `if(_hasResult${tapIndex
}
) throw _err${tapIndex
}
;
\n`
;
code += onError(`_err${tapIndex
}
`
);
code += "
});
\n"
;
break;

} return code;

}複製程式碼

也是對應的分成 sync/async/promise ,上面程式碼翻譯成圖,如下

  • sync 型別:
sync
  • async 型別:
async
  • promise 型別
promise

總的來看, callTap 內是一次函式執行的模板,也是根據呼叫方式的不同,分為 sync/async/promise 三種。

然後看 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 和 onDone 的判斷條件,就是說有 onResult 或者 onDone onResult: onResult &
&
((result) =>
{
return onResult(i, result, done, doneBreak);

}), onDone: !onResult &
&
(() =>
{
return done();

}), rethrowIfPossible: rethrowIfPossible &
&
(firstAsync <
0 || i <
firstAsync)
});

};
return next(0);

}複製程式碼

注意看 this.callTap 中 onResult 和 onDone 的條件,就是說要麼執行 onResult, 要麼執行 onDone。先看簡單的直接走 onDone 的邏輯。那麼結合上面 callTap 的流程,以 sync 為例,可以得到下面的圖:

sync流程

對於這種情況,callTapsSeries 的結果是遞迴的生成每一次的呼叫 code,直到最後一個時,直接呼叫外部傳入的 onDone 方法得到結束的 code, 遞迴結束。而對於執行 onResult 的流程,看一下 onResult 程式碼: return onResult(i, result, done, doneBreak)。簡單理解,和上面圖中流程一樣的,只不過在 done 的外面用 onResult 包裹了一層關於 onResult 的邏輯。

接著我們看 callTapsLooping 的程式碼:

callTapsLooping({ 
onError, onDone, rethrowIfPossible
}) {
if(this.options.taps.length === 0) return onDone();
const syncOnly = this.options.taps.every(t =>
t.type === "sync");
let code = "";
if(!syncOnly) {
code += "var _looper = () =>
{\n"
;
code += "var _loopAsync = false;
\n"
;

} // 在程式碼開始前加入 do 的邏輯 code += "var _loop;
\n"
;
code += "do {\n";
code += "_loop = false;
\n"
;
// interceptors 先忽略,只看主要部分 for(let i = 0;
i <
this.options.interceptors.length;
i++) {
const interceptor = this.options.interceptors[i];
if(interceptor.loop) {
code += `${this.getInterceptor(i)
}
.loop(${this.args({
before: interceptor.context ? "_context" : undefined
}
)
});
\n`
;

}
} code += this.callTapsSeries({
onError, onResult: (i, result, next, doneBreak) =>
{
let code = "";
code += `if(${result
}
!== undefined) {\n`
;
code += "_loop = true;
\n"
;
if(!syncOnly) code += "if(_loopAsync) _looper();
\n"
;
code += doneBreak(true);
code += `
} else {\n`
;
code += next();
code += `
}\n`
;
return code;

}, onDone: onDone &
&
(() =>
{
let code = "";
code += "if(!_loop) {\n";
code += onDone();
code += "
}\n"
;
return code;

}), rethrowIfPossible: rethrowIfPossible &
&
syncOnly
}) code += "
} while(_loop);
\n"
;
if(!syncOnly) {
code += "_loopAsync = true;
\n"
;
code += "
};
\n"
;
code += "_looper();
\n"
;

} return code;

}複製程式碼

先簡化到最簡單的邏輯就是下面這段,很簡單的 do/while 邏輯。

var _loopdo { 
_loop = false // callTapsSeries 生成中間部分程式碼
} while(_loop)複製程式碼

callTapsSeries 前面瞭解了其程式碼,這裡呼叫 callTapsSeries 時,有 onResult 邏輯,也就是說中間部分會生成類似下面的程式碼(仍是以 sync 為例)

var _fn${tapIndex
} = _x[${tapIndex
}];
var _hasError${tapIndex
} = false;
try {
fn1(${this.args({
before: tap.context ? "_context" : undefined
})
});

} catch(_err) {
_hasError${tapIndex
} = true;
onError("_err");

}if(!_hasError${tapIndex
}) {
// onResult 中生成的程式碼 if(${result
} !== undefined) {
_loop = true;
// doneBreak 位於 callTapsSeries 程式碼中 //(skipDone) =>
{
// if(skipDone) return "";
// return onDone();
//
}
doneBreak(true);
// 實際為空語句
} else {
next()
}
}複製程式碼

通過在 onResult 中控制函式執行完成後到執行下一個函式之間,生成程式碼的不同,就從 callTapsSeries 中衍生出了 LoopHook 的邏輯。

然後我們看 callTapsParallel

callTapsParallel({ 
onError, onResult, onDone, rethrowIfPossible, onTap = (i, run) =>
run()
}) {
if(this.options.taps.length <
= 1) {
return this.callTapsSeries({
onError, onResult, onDone, rethrowIfPossible
})
} let code = "";
code += "do {\n";
code += `var _counter = ${this.options.taps.length
}
;
\n`
;
if(onDone) {
code += "var _done = () =>
{\n"
;
code += onDone();
code += "
};
\n"
;

} for(let i = 0;
i <
this.options.taps.length;
i++) {
const done = () =>
{
if(onDone) return "if(--_counter === 0) _done();
\n"
;
else return "--_counter;
"
;

};
const doneBreak = (skipDone) =>
{
if(skipDone || !onDone) return "_counter = 0;
\n"
;
else return "_counter = 0;
\n_done();
\n"
;

} code += "if(_counter <
= 0) break;
\n"
;
code += onTap(i, () =>
this.callTap(i, {
onError: error =>
{
let code = "";
code += "if(_counter >
0) {\n"
;
code += onError(i, error, done, doneBreak);
code += "
}\n"
;
return code;

}, onResult: onResult &
&
((result) =>
{
let code = "";
code += "if(_counter >
0) {\n"
;
code += onResult(i, result, done, doneBreak);
code += "
}\n"
;
return code;

}), onDone: !onResult &
&
(() =>
{
return done();

}), rethrowIfPossible
}), done, doneBreak);

} code += "
} while(false);
\n"
;
return code;

}複製程式碼

由於 callTapsParallel 最終生成的程式碼是併發執行的,那麼程式碼流程就和兩個差異較大。上面程式碼看起來較多,捋一下主要結構,其實就是下面的圖(仍是以 sync 為例)

callTapsParallel

總結一下 callTap 中實現了 sync/promise/async 三種基本的一次函式執行的模板,同時將涉及函式執行流程的程式碼 onError/onDone/onResult 部分留出來。而 callTapsSeries/callTapsLooping/callTapsParallel 中,通過傳入不同的 onError/onDone/onResult 實現出不同流程的模板。不過 callTapsParallel 由於差異較大,通過在 callTap 外包裹一層 onTap 函式,對生成的結果進行再次加工。

到此,我們得到了 series/looping/parallel 三大類基礎模板。我們注意到,callTapsSeries/callTapsLooping/callTapsParallel 中同時也暴露出了自己的 onError, onResult, onDone, rethrowIfPossible, onTap,由此來實現每個子 Hook 根據不同情況對基礎模板進行定製。以 SyncBailHook 為例,它和 callTapsSeries 得到的基礎模板的主要區別在於函式執行結束時機不同。因此對於 SyncBailHook 來說,修改 onResult 即可達到目的:

class SyncBailHookCodeFactory extends HookCodeFactory { 
content({
onError, onResult, onDone, rethrowIfPossible
}) {
return this.callTapsSeries({
onError: (i, err) =>
onError(err), // 修改一下 onResult,如果 函式執行得到的 result 不為 undefined 則直接返回結果,否則繼續執行下一個函式 onResult: (i, result, next) =>
`if(${result
}
!== undefined) {\n${onResult(result)
}
;
\n
} else {\n${next()
}

}\n`, onDone, rethrowIfPossible
});

}
}複製程式碼

最後我們來用一張圖,整體的總結一下 compile 部分生成最終執行程式碼的思路:總結出通用的程式碼模板,將差異化部分拆分到函式中並且暴露給外部來實現。

整體程式碼生成

總結

相比於簡單的 EventEmit 來說,Tapable 作為 webpack 底層事件流庫,提供了豐富的事件。而最終事件觸發後的執行,是先動態生成執行的 code,然後通過 new Function 來執行。相比於我們平時直接遍歷或者遞迴的呼叫每一個事件來說,這種執行方法效率上來說相對更高效。雖然平時寫程式碼時,對於一個迴圈,是拆開來寫每一個還是直接 for 迴圈,在效率上來說看不出什麼,但是對 webpack 來說,由於其整體是由事件機制推動,內部存在大量這樣的邏輯。那麼這種拆開來直接執行每一個函式的方式,便可看出其優勢所在。

來源:https://juejin.im/post/5c25f920e51d45593b4bc719

相關文章