webpack 是基於事件流的打包構建工具,也就是內置了不少 hooks。做爲使用方,能夠在這些鉤子當中,去插入本身的處理邏輯,而這一切的實現都得益於 tapable 這個工具。它有多個版本,webpack 前期的版本是依賴於 tapable 0.2.8 這個版本,後來重構了,發了 2.0.0 beta 版本,由於源碼都是經過字符串拼接,經過 new Function 的模式使用,因此看起來比較晦澀。html
那麼既然如此,咱們先從早期的 0.2.8 這個版本瞭解下它的前身,畢竟核心思想不會發生太大的變化。node
tapable 的實現相似於 node 的 EventEmitter 的發佈訂閱模式。用一個對象的鍵存儲對應的事件名稱,鍵值來存儲事件的處理函數,相似於下面:webpack
function Tapable () {
this._plugins = {
'emit': [handler1, handler2, ......]
}
}
複製代碼
同時,原型上定義了不一樣的方法來調用 handlers。git
咱們先來看下用法,github
plugin 與 applyPluginsweb
void plugin(names: string|string[], handler: Function)
void applyPlugins(name: string, args: any...)
複製代碼
最基礎的就是註冊插件以及插件觸發的回調函數。express
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 版的重構更是讓人驚豔,目前的源碼分析正在整理,連接以下。