非同步編程學習筆記之Tapable源碼分析
來自專欄前端學習筆記
本文主要目的是學習如何通過error-first風格的回調函數組織非同步代碼的執行流程,並非是對webpack的介紹。此文為非同步編程學習筆記的第一篇筆記,後續將對非同步編程相關的知識點如event loop,Promise,generator,async等做總結,最終形成一個系列。
1.1 Tapable簡介
webpack通過插件(plugin)擴展其功能,自身也是構建於插件系統之上。webpack中的許多對象擴展自Tapable類,並暴露了出plugin方法。通過plugin方法,插件可以注入自定義構建步驟。開發 Plugin 時最常用的兩個對象: Compiler 和 Compilation也繼承自Tapable。
Tapable是一個JavaScript庫,用於添加和調用插件。它可以被繼承(extend)或混入(mixin)到其他模塊。功能類似於NodeJS中的EventEmitter,用於自定義發射和操作事件。在Tapable中,可以通過回調函數的參數獲取對應事件的執行結果。
Tapable函數分類
plugin(name:string, handler:function)
:此方法與EventEmitter中on()方法類型,用於註冊處理函數。通過將自定義插件註冊到Tapable實例的事件中,以便在事件發生時執行相應操作。apply(…pluginInstances: (AnyPlugin|function)[])
: anyPlugin可以是一個帶有apply方法的類或對象,也可以是一個普通函數(一般來說,所有JavaScript函數都可以通過原型鏈訪問apply屬性)。webpack中插件的apply方法內部通常用於註冊/監聽相應事件的處理函數。applyPlugins*(name:string, …)
:applyPlugins*包含一系列函數,Tapable實例可以通過這些函數調用對應事件下的所有插件。這組方法與EventEmitter的emit()方法類似,可以使用多種不同的策略控制插件的執行流程。mixin(pt: Object)
:此方法將Tapable的原型對象混入(mixin)到自定義類或對象中。
不同的
applyPlugins*
方法包含以下幾種插件執行控制流程
- 順序執行
- 並發執行
- 順序運行(waterfall),後一個插件從前一個插件獲取輸入。
- 非同步運行。
- 存在輸出的情況下退出(bail)
1.2Tapable使用
- 通過繼承的方式使用Tapable
function MyClass() { Tapable.call(this);}// 此寫法源自其README文檔,會導致MyClass的原型對象中constructor屬性被覆蓋MyClass.prototype = Object.create(Tapable.prototype);MyClass.prototype.method = function() {};
- 通過混入的方式使用Tapable
function MyClass2() { EventEmitter.call(this); Tapable.call(this);}// minix通過屬性複製的方式實現在多重繼承的功能// 但此處會導致instanceof操作符失效MyClass2.prototype = Object.create(EventEmitter.prototype);Tapable.mixin(MyClass2.prototype);MyClass2.prototype.method = function() {};
2 Tapable源碼分析
webpack v3.10.0中使用的Tapable版本為0.2.8,且最新版本v1.0beta版改動很大。本筆記使用的源碼為v0.2.8版本
- fastFilter函數:功能等價於Array.prototype.filter
1. 此函數是一個墊片函數(polyfill),出於性能考慮而避免調用Object.defineProperty
2. filter是ECMA-262標準第五版中定義的,因此某些實現環境中可能不支持,該代碼可以保證沒有原生支持filter的環境中使用。
function fastFilter(fun/*, thisArg*/) { // thisArg為callback執行時的this值 use strict; // void是JavaScript中的操作符,用於計算給定的表達式並返回undefined if (this === void 0 || this === null) { throw new TypeError(); } var t = Object(this); // 字元串轉數字的幾種方式如下,區別主要在於對邊界情況的返回結果不同 // Number(value) // parseInt/parseFloat(value) // +value // value >>> 0 var len = t.length >>> 0; if (typeof fun !== function) { throw new TypeError(); } var res = []; var thisArg = arguments.length >= 2 ? arguments[1] : void 0; for (var i = 0; i < len; i++) { if (i in t) { var val = t[i]; if (fun.call(thisArg, val, i, t)) { // 此處可以使用Object.defineProperty定義 res.push(val); } } } return res;}
copyProperties(from: Object, to: Object)
:將from對象上的屬性複製到to對象上
功能類似於Object.assign,但
for ... in
語句會導致源對象原型鏈中的可遍歷屬性也被複制到目標對象上。
function copyProperties(from, to) { for(var key in from) to[key] = from[key]; return to;}
Tapable()
:構造函數
生成的實例中僅包含
_plugins
屬性 ,用於保存通過plugin
函數註冊的處理函數
function Tapable() { this._plugins = {};}
Tapable.prototype.plugin(names: string|string[], handler, Function)
: 對指定的名稱註冊處理函數
names可以是單個字元串,也可以是一個數組。當names為數組時,為數組中的每一項註冊一次處理函數。
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);}
Tapable.prototype.hasPlugins(name: string)
: 檢查指定的名稱是否註冊對應的處理函數
function hasPlugins(name) { var plugins = this._plugins[name]; return plugins && plugins.length > 0;}
Tapable.prototype.apply(plugins: Plugin)
:依次調用傳入參數中的apply方法
在webpack中,plugin都包含apply方法,用於為插件實例註冊處理函數
function apply() { for(var i = 0; i < arguments.length; i++) { arguments[i].apply(this); }}
Tapable.mixin(pt: Object)
:將Tapable中原型對象上的屬性複製到目標對象pt中
function mixinTapable(pt) { copyProperties(Tapable.prototype, pt);}
源碼核心部分
以下部分是本筆記重點,分析Tapable中
applyPlugins*()
如何使用不同的策略控制插件的執行流程。
Tapable.prototype.applyPlugins(name: string, args: any)
:同步調用指定名稱註冊的所有處理函數
1. 通過name獲取之前註冊的處理函數數組
2. 按數組順序依次調用插件,插件同步執行3. 每次調用處理函數均傳入相同的參數args,忽略/不存在返回值
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.applyPliginsWaterfall(name: string, args: any...)
: 同步調用指定名稱註冊的所有處理函數
1. 通過name獲取之前註冊的處理函數數組
2. 按數組順序依次調用插件,插件同步執行
3. 調用處理函數時傳入的參數為上一個插件返回的結果4. 對於第一個處理函數參數為init,最終返回值為最後一個插件的返回值
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;}
Tapable.prototype.applyPluginsBailResult(name: string, args: any)
:同步調用指定名稱註冊的所有處理函數
1. 通過name獲取之前註冊的處理函數數組
2. 按數組順序依次調用插件,插件同步執行3. 每次調用處理函數均傳入相同的參數args
4. 如果某一處理函數返回值為非undefined,返回此值,終端函數執行
function applyPluginsBailResult(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++) { var result = plugins[i].apply(this, args); if(typeof result !== "undefined") { return result; } }}
Tapable.prototype.applyPluginAsyncSeries(name: string, args: any, callback: (err: Error, result: any) -> void)
: 非同步的依次調用指定名稱註冊的所有處理函數
1. 通過name獲取之前註冊的處理函數數組
2. 按數組順序依次調用插件,插件非同步執行3. 每次調用處理函數均傳入相同的參數args4. 通過注入的next函數保證前一個插件完成後調用下一個插件,
5. 如果某個插件執行出現錯誤,直接使用錯誤參數調用回調函數,後續插件不再執行
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);}
Tapable.prototype.applyPluginAsyncSeriesBailResult(name: string, args: any..., callback: (err: Error, result: any) -> void)
: 非同步的依次調用指定名稱註冊的所有處理函數
1. 通過name獲取之前註冊的處理函數數組
2. 按數組順序依次調用插件,插件非同步執行3. 每次調用處理函數均傳入相同的參數args
3. 通過注入的next函數保證前一個插件完成後調用下一個插件,4. 如果某個插件執行出現錯誤或包含非undefined值,直接使用錯誤參數或非undefined值調用回調函數,後續插件不再執行
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);}
Tapable.prototype.applyPluginsAsyncWaterfall(name: string, init: any, callback: (err: Error, result: any) -> void)
:非同步的依次調用指定名稱註冊的所有處理函數
1. 通過name獲取之前註冊的處理函數數組
2. 按數組順序依次調用插件,插件非同步執行3. 通過注入的next函數保證前一個插件完成後調用下一個插件4. 調用處理函數時傳入的參數為上一個插件通過next函數傳入的結果5. 對於第一個處理函數參數為init,最終調用callback的參數為最後一個plugin調用next傳入的參數
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);}
Tapable.prototype.applyPluginsParallel(name: string, args: any..., callback: (err?: Error) -> void)
: 非同步並發調用指定名稱註冊的所有處理函數
1. 通過name獲取之前註冊的處理函數數組
2. 按數組順序並發調用插件,插件非同步執行3. 每次調用處理函數均傳入相同的參數args4. 通過注入的next函數保證某一插件處理錯誤時調用以該錯誤調用callback,並忽略其餘插件的執行;否則,待所有插件執行完成後,調用callback5. 最中callback調用僅可能包含一個錯誤原因
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; }}
Tapable.prototype.applyPluginsParallelBailResult(name: string, args: any..., callback: (error: Error, result: any) -> void)
: 非同步並發調用指定名稱註冊的所有處理函數
1. 通過name獲取之前註冊的處理函數數組
2. 按數組順序並發調用插件,插件非同步執行3. 每次調用處理函數均傳入相同的參數args4. 通過注入的next函數保證某一插件執行存在錯誤或執行結果時,忽略後續所有處理程序的執行結果,並以該錯誤或執行結果調用callback。
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); }}
推薦閱讀:
※前端日刊-2017.12.30
※零基礎轉行學習web前端遇見的那些坑要如何避免
※如何用 55 行代碼實現一個簡單的 View 框架
※Daguo的每周清單:第四期
※大眾點評長期招前端