非同步編程學習筆記之Tapable源碼分析

非同步編程學習筆記之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. 每次調用處理函數均傳入相同的參數args

4. 通過注入的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. 每次調用處理函數均傳入相同的參數args

4. 通過注入的next函數保證某一插件處理錯誤時調用以該錯誤調用callback,並忽略其餘插件的執行;否則,待所有插件執行完成後,調用callback

5. 最中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. 每次調用處理函數均傳入相同的參數args

4. 通過注入的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的每周清單:第四期
大眾點評長期招前端

TAG:前端開發框架和庫 | 前端框架 | 前端開發 |