webpack 是基於事件流的打包構建工具,也就是內建了很多 hooks。作為使用方,可以在這些鉤子當中,去插入自己的處理邏輯,而這一切的實現都得益於 tapable 這個工具。它有多個版本,webpack 前期的版本是依賴於 tapable 0.2.8 這個版本,後來重構了,發了 2.0.0 beta 版本,因為原始碼都是通過字串拼接,通過 new Function 的模式使用,所以看起來比較晦澀。
那麼既然如此,我們先從早期的 0.2.8 這個版本瞭解下它的前身,畢竟核心思想不會發生太大的變化。
tapable 的實現類似於 node 的 EventEmitter 的釋出訂閱模式。用一個物件的鍵儲存對應的事件名稱,鍵值來儲存事件的處理函式,類似於下面:
function Tapable () {
this._plugins = {
`emit`: [handler1, handler2, ......]
}
}
複製程式碼
同時,原型上定義了不同的方法來呼叫 handlers。
我們先來看下用法,
-
plugin 與 applyPlugins
void plugin(names: string|string[], handler: Function) void applyPlugins(name: string, args: any...) 複製程式碼
最基礎的就是註冊外掛以及外掛觸發的回撥函式。
const Tapable = require(`tapable`) const t = new Tapable() // 註冊外掛 t.plugin(`emit`, (...args) => { console.log(args) console.log(`This is a emit handler`) }) // 呼叫外掛 t.applyPlugins(`emit`, `引數1`) // 列印如下 [ `引數1` ] This is a emit handler 複製程式碼
原始碼如下
Tapable.prototype.applyPlugins = function applyPlugins(name) { if(!this._plugins[name]) return; var args = Array.prototype.slice.call(arguments, 1); var plugins = this._plugins[name]; for(var i = 0; i < plugins.length; i++) plugins[i].apply(this, args); }; Tapable.prototype.plugin = function plugin(name, fn) { if(Array.isArray(name)) { name.forEach(function(name) { this.plugin(name, fn); }, this); return; } if(!this._plugins[name]) this._plugins[name] = [fn]; else this._plugins[name].push(fn); }; 複製程式碼
很簡單,內部維護 _plugins 屬性來快取 plugin 名稱以及 handler。
-
apply
void apply(plugins: Plugin...) 複製程式碼
接收 plugin 作為引數,每個 plugin 必須提供 apply 方法,也就是 webpack 在編寫 plugin 的規是外掛例項必須提供 apply 方法。
const Tapable = require(`tapable`) const t = new Tapable() // 宣告一個 webpack 外掛的類,物件必須宣告 apply 方法 class WebpackPlugin { constructor () {} apply () { console.log(`This is webpackPlugin`) } } const plugin = new WebpackPlugin() // tapable.apply t.apply(plugin) // print `This is webpackPlugin` 複製程式碼
原始碼如下
Tapable.prototype.apply = function apply() { for(var i = 0; i < arguments.length; i++) { arguments[i].apply(this); } }; 複製程式碼
也很簡單,依次執行每個外掛的 apply 方法。
-
applyPluginsWaterfall
any applyPluginsWaterfall(name: string, init: any, args: any...) 複製程式碼
依次呼叫外掛對應的 handler,傳入的引數是上一個 handler 的返回值,以及呼叫 applyPluginsWaterfall 傳入 args 引數組成的陣列,說起來很繞,看看下面的例子:
t.plugin(`waterfall`, (...args) => { // print [`init`, `args1`] console.log(args) return `result1` }) t.plugin(`waterfall`, (...args) => { // print [`result1`, `args1`] console.log(args) return `result2` }) const ret = t.applyPluginsWaterfall(`waterfall`, `init`, `args1`) // ret => `result2` 複製程式碼
原始碼如下
Tapable.prototype.applyPluginsWaterfall = function applyPluginsWaterfall(name, init) { if(!this._plugins[name]) return init; var args = Array.prototype.slice.call(arguments, 1); var plugins = this._plugins[name]; var current = init; for(var i = 0; i < plugins.length; i++) { args[0] = current; current = plugins[i].apply(this, args); } return current; }; 複製程式碼
上一個 handler 返回的值,會作為下一個 handler的第一個引數。
-
applyPluginsBailResult
any applyPluginsBailResult(name: string, args: any...) 複製程式碼
依次呼叫外掛對應的 handler,傳入的引數是 args,如果正執行的 handler 的 返回值不是 undefined,其餘的 handler 都不會執行了。
bail
是保險的意思,即只要任意一個 handler 有!== undefined
的返回值,那麼函式的執行就終止了。t.plugin(`bailResult`, (...args) => { // [ `引數1`, `引數2` ] console.log(args) return `result1` }) t.plugin(`bailResult`, (...args) => { // 因為上一個函式返回了 `result1`,所以不會執行到這個handler console.log(args) return `result2` }) t.applyPluginsBailResult(`bailResult`, `引數1`, `引數2`) 複製程式碼
原始碼如下
Tapable.prototype.applyPluginsBailResult = function applyPluginsBailResult(name, init) { if(!this._plugins[name]) return; var args = Array.prototype.slice.call(arguments, 1); var plugins = this._plugins[name]; for(var i = 0; i < plugins.length; i++) { var result = plugins[i].apply(this, args); if(typeof result !== "undefined") { return result; } } }; 複製程式碼
只要 handler 返回的值
!== undefined
,就會停止呼叫接下來的 handler。 -
applyPluginsAsyncSeries & applyPluginsAsync(支援非同步)
void applyPluginsAsync( name: string, args: any..., callback: (err?: Error) -> void ) 複製程式碼
applyPluginsAsyncSeries 與 applyPluginsAsync 的函式引用都是相同的,並且函式內部支援非同步。callback 在所有 handler 都執行完了才會呼叫,但是在註冊 handler 的時候,函式內部一定要執行 next() 的邏輯,這樣才能執行到下一個 handler。
t.plugin(`asyncSeries`, (...args) => { // handler 的最後一個引數一定是 next 函式 const next = args.pop() // 執行 next,函式才會執行到下面的 handler setTimeout (() => { next() }, 3000) }) t.plugin(`asyncSeries`, (...args) => { // handler 的最後一個引數一定是 next const callback = args.pop() // 執行 next,函式才會執行到 applyPluginsAsyncSeries 傳入的 callback Promise.resolve(1).then(next) }) t.applyPluginsAsyncSeries(`asyncSeries`, `引數1`, (...args) => { console.log(`這是 applyPluginsAsyncSeries 的 callback`) }) 複製程式碼
原始碼如下
Tapable.prototype.applyPluginsAsyncSeries = Tapable.prototype.applyPluginsAsync = function applyPluginsAsyncSeries(name) { var args = Array.prototype.slice.call(arguments, 1); var callback = args.pop(); var plugins = this._plugins[name]; if(!plugins || plugins.length === 0) return callback(); var i = 0; var _this = this; args.push(copyProperties(callback, function next(err) { if(err) return callback(err); i++; if(i >= plugins.length) { return callback(); } plugins[i].apply(_this, args); })); plugins[0].apply(this, args); }; 複製程式碼
applyPluginsAsyncSeries 內部維護了一個 next 函式,這個函式作為每個 handler 的最後一個引數傳入,handler 內部支援非同步操作,但是必須手動呼叫 next 函式,才能執行到下一個 handler。
-
applyPluginsAsyncSeriesBailResult(支援非同步)
void applyPluginsAsyncSeriesBailResult( name: string, args: any..., callback: (result: any) -> void ) 複製程式碼
函式支援非同步,只要在 handler 裡面呼叫 next 回撥函式,並且傳入任意引數,就會直接執行 callback。
t.plugin(`asyncSeriesBailResult`, (...args) => { // handler 的最後一個引數一定是 next 函式 const next = args.pop() // 因為傳了字串,導致直接執行 callback next(`跳過 handler 函式`) }) t.plugin(`asyncSeriesBailResult`, (...args) => { }) t.applyPluginsAsyncSeriesBailResult(`asyncSeriesBailResult`, `引數1`, (...args) => { console.log(`這是 applyPluginsAsyncSeriesBailResult 的 callback`) }) // print `這是 applyPluginsAsyncSeriesBailResult 的 callback` 複製程式碼
原始碼如下
Tapable.prototype.applyPluginsAsyncSeriesBailResult = function applyPluginsAsyncSeriesBailResult(name) { var args = Array.prototype.slice.call(arguments, 1); var callback = args.pop(); if(!this._plugins[name] || this._plugins[name].length === 0) return callback(); var plugins = this._plugins[name]; var i = 0; var _this = this; args.push(copyProperties(callback, function next() { if(arguments.length > 0) return callback.apply(null, arguments); i++; if(i >= plugins.length) { return callback(); } plugins[i].apply(_this, args); })); plugins[0].apply(this, args); }; 複製程式碼
applyPluginsAsyncSeriesBailResult 內部維護了一個 next 函式,這個函式作為每個 handler 的最後一個引數傳入,handler 內部支援非同步操作,但是必須手動呼叫 next 函式,才能執行到下一個 handler,next 函式可以傳入引數,這樣會直接執行 callback。
-
applyPluginsAsyncWaterfall(支援非同步)
void applyPluginsAsyncWaterfall( name: string, init: any, callback: (err: Error, result: any) -> void ) 複製程式碼
函式支援非同步,handler 的接收兩個引數,第一個引數是上一個 handler 通過 next 函式傳過來的 value,第二個引數是 next 函式。next 函式接收兩個引數,第一個是 error,如果 error 存在,就直接執行 callback。第二個 value 引數,是傳給下一個 handler 的引數。
t.plugin(`asyncWaterfall`, (value, next) => { // handler 的最後一個引數一定是 next 函式 console.log(value) next(null, `來自第一個 handler`) }) t.plugin(`asyncWaterfall`, (value, next) => { console.log(value) next(null, `來自第二個 handler`) }) t.applyPluginsAsyncWaterfall(`asyncWaterfall`, `引數1`, (err, value) => { if (!err) { console.log(value) } }) // 列印如下 引數1 來自第一個 handler 來自第二個 handler 複製程式碼
原始碼如下
Tapable.prototype.applyPluginsAsyncWaterfall = function applyPluginsAsyncWaterfall(name, init, callback) { if(!this._plugins[name] || this._plugins[name].length === 0) return callback(null, init); var plugins = this._plugins[name]; var i = 0; var _this = this; var next = copyProperties(callback, function(err, value) { if(err) return callback(err); i++; if(i >= plugins.length) { return callback(null, value); } plugins[i].call(_this, value, next); }); plugins[0].call(this, init, next); }; 複製程式碼
applyPluginsAsyncWaterfall 內部維護了一個 next 函式,這個函式作為每個 handler 的最後一個引數傳入,handler 內部支援非同步操作,但是必須手動呼叫 next 函式,才能執行到下一個 handler,next 函式可以傳入引數,第一個引數為 err, 第二引數為上一個 handler 返回值。
-
applyPluginsParallel(支援非同步)
void applyPluginsParallel( name: string, args: any..., callback: (err?: Error) -> void ) 複製程式碼
並行的執行函式,每個 handler 的最後一個引數都是 next 函式,這個函式用來檢驗當前的 handler 是否已經執行完。
t.plugin(`parallel`, (...args) => { const next = args.pop() console.log(1) // 必須呼叫 next 函式,要不然 applyPluginsParallel 的 callback 永遠也不會回撥 next(`丟擲錯誤了1`, `來自第一個 handler`) }) t.plugin(`parallel`, (...args) => { const next = args.pop() console.log(2) // 必須呼叫 next 函式,要不然 applyPluginsParallel 的 callback 永遠也不會回撥 next(`丟擲錯誤了2`) }) t.applyPluginsParallel(`parallel`, `引數1`, (err) => { // print `丟擲錯誤了1` console.log(err) }) 複製程式碼
原始碼如下
Tapable.prototype.applyPluginsParallel = function applyPluginsParallel(name) { var args = Array.prototype.slice.call(arguments, 1); var callback = args.pop(); if(!this._plugins[name] || this._plugins[name].length === 0) return callback(); var plugins = this._plugins[name]; var remaining = plugins.length; args.push(copyProperties(callback, function(err) { if(remaining < 0) return; // ignore if(err) { remaining = -1; return callback(err); } remaining--; if(remaining === 0) { return callback(); } })); for(var i = 0; i < plugins.length; i++) { plugins[i].apply(this, args); if(remaining < 0) return; } }; 複製程式碼
applyPluginsParallel 並行地呼叫 handler。內部通過閉包維護了 remaining 變數,用來判斷內部的函式是否真正執行完,handler 的最後一個引數是一個函式 check。如果 handler 內部使用者想要的邏輯執行完,必須呼叫 check 函式來告訴 tapable,進而才會執行 args 陣列的最後一個 check 函式。
-
** applyPluginsParallelBailResult **(支援非同步)
void applyPluginsParallelBailResult( name: string, args: any..., callback: (err: Error, result: any) -> void ) 複製程式碼
並行的執行函式,每個 handler 的最後一個引數都是 next 函式,next 函式必須呼叫,如果給 next 函式傳參,會直接走到 callback 的邏輯。callback 執行的時機是跟 handler 註冊的順序有關,而不是跟 handler 內部呼叫 next 的時機有關。
t.plugin(`applyPluginsParallelBailResult`, (next) => { console.log(1) setTimeout(() => { next(`has args 1`) }, 3000) }) t.plugin(`applyPluginsParallelBailResult`, (next) => { console.log(2) setTimeout(() => { next(`has args 2`) }) }) t.plugin(`applyPluginsParallelBailResult`, (next) => { console.log(3) next(`has args 3`) }) t.applyPluginsParallelBailResult(`applyPluginsParallelBailResult`, (result) => { console.log(result) }) // 列印如下 1 2 3 has args 1 雖然第一個 handler 的 next 函式是延遲 3s 才執行,但是註冊的順序是在最前面,所以 callback 的 result 引數值是 `has args 1`。 複製程式碼
原始碼如下
Tapable.prototype.applyPluginsParallelBailResult = function applyPluginsParallelBailResult(name) { var args = Array.prototype.slice.call(arguments, 1); var callback = args[args.length - 1]; if(!this._plugins[name] || this._plugins[name].length === 0) return callback(); var plugins = this._plugins[name]; var currentPos = plugins.length; var currentResult; var done = []; for(var i = 0; i < plugins.length; i++) { args[args.length - 1] = (function(i) { return copyProperties(callback, function() { if(i >= currentPos) return; // ignore done.push(i); if(arguments.length > 0) { currentPos = i + 1; done = fastFilter.call(done, function(item) { return item <= i; }); currentResult = Array.prototype.slice.call(arguments); } if(done.length === currentPos) { callback.apply(null, currentResult); currentPos = 0; } }); }(i)); plugins[i].apply(this, args); } }; 複製程式碼
for 迴圈裡面並行的執行 handler,handler 的最後一個引數是一個匿名回撥函式,這個匿名函式必須在每個 handler 裡面手動的執行。而 callback 的執行時機就是根據 handler 的註冊順序有關。
從原始碼上來看,tapable 是提供了很多 API 來對應不同呼叫 handler 的場景,有同步執行,有非同步執行,還有序列非同步,並行非同步等。這些都是一些高階的技巧,不管是 express,還是 VueRouter 的原始碼,都利用這些同非同步執行機制,但是可以看出程式是有邊界的。也就是約定成俗,從最後一個 applyPluginsParallel 函式來看,使用者必須呼叫匿名回撥函式,否則 tapable 怎麼知道你內部是否有非同步操作,並且非同步操作在某個時候執行完了呢。
既然知道了 0.2.8 的核心思想,那麼 2.0.0-beta 版的重構更是讓人驚豔,目前的原始碼分析正在整理,連結如下。