Tapable 0.2.8 入門

 【原文:Tapable 0.2.8 入門javascript

tapable是webpack的核心框架(4.0以上版本的API已經發生了變化),是一個基於事件流的框架,或者叫作發佈訂閱模式,或觀察者模式,webpack的整個生命週期及其開放的自定義插件系統都離不開tapable的支持,研究其運行原理是閱讀webpack源代碼的第一步。html

 

Tapable是一個小型庫,容許你添加和應用插件到一個javascript模塊。它能夠被繼承或混入其餘模塊。除能夠定製事件發射和操做,還能夠經過回調參數訪問事件的「排放者」或「生產者」。java

 

Tapable 有四組成員函數:node

  • plugin(name:string, handler:function):這容許自定義插件註冊到Tapable實例的事件中。這起到相似on()的方法EventEmitter,這是用於註冊一個處理程序/偵聽器當信號/事件發生作一些事情。
  • apply(…pluginInstances: (AnyPlugin|function)[]):AnyPlugin應該是一個具備方法的類(或者不多,是一個對象)apply,或者只是一個帶有一些註冊碼的函數。此方法僅適用於插件的定義,以便真正的事件偵聽器能夠註冊到Tapable實例的註冊表中。
  • applyPlugins*(name:string, …):Tapable實例可使用這些函數將特定哈希下的全部插件應用。這組方法就像使用各類策略精心控制事件發射的emit()方法EventEmitter。
  • mixin(pt: Object):一種簡單的方法來將Tapable原型擴展爲mixin而不是繼承。

 

不一樣的applyPlugins*方法涵蓋如下用例:webpack

  • 插件能夠串行運行。
  • 插件能夠並行運行。
  • 插件能夠一個接一個地運行,但從前一個插件(瀑布)獲取輸入。
  • 插件能夠異步運行。
  • 放棄保釋運行插件:也就是說,一旦一個插件返回非插件undefined,跳出運行流程並返回該插件的返回。這聽起來像once()的EventEmitter,可是是徹底不一樣的。

 

 

Tapable 介紹

var Tapable = require("tapable"); 

Tapable 是一個庫,用於綁定和執行插件。git

在使用上,你僅僅須要繼承它github

function MyClass() { Tapable.call(this) } MyClass.prototype = Object.create(Tapable.prototype) MyClass.prototype.method = function() {} 

或者複製它的屬性到你的類中web

function MyClass2() { EventEmitter.call(this); Tapable.call(this); } MyClass2.prototype = Object.create(EventEmitter.prototype); Tapable.mixin(MyClass2.prototype); MyClass2.prototype.method = function() {}; 

 

公開方法(Public functions)

apply

void apply(plugins: Plugin...) Tapable.prototype.apply = function apply() { for(var i = 0; i < arguments.length; i++) { arguments[i].apply(this); // 遍歷全部參數並執行 } }; 

經過arguments得到全部傳入的插件對象,並調用插件對象的apply方法,更改上下文爲當前this,執行插件。(Webpack的插件就是Tapable對象,所以必需要提供 apply 方法)segmentfault

plugin

void plugin(names: string|string[], handler: Function) 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); }; 

names: 須要監聽的事件名稱,能夠傳入事件名稱集合,也能夠傳入單個事件名稱 handler: 事件的處理函數數組

tapable經過原型方法Tapable.prototype.plugin來註冊事件監聽。將回調函數按照事件名稱進行歸類存儲,在tapable實例中統一調度管理。

 

受保護的方法 (Protected functions)

  • applyPlugins

  • applyPluginsWaterfall

  • applyPluginsBailResult

  • applyPluginsAsync

  • applyPluginsAsyncWaterfall

  • applyPluginsAsyncSeries

  • applyPluginsParallel

  • applyPluginsParallelBailResult

  • hasPlugins

tapable中的事件觸發方式能夠按命名分爲以下幾個大組:

  • waterfall方法會將上一個監聽的執行結果傳給下一個
  • bailResult方法只會執行到第一個返回結果不是undefined的事件流
  • Series方法會線性執行異步事件流,上一個結束後下一個纔會開始
  • Parallel方法會並行執行全部異步監聽

applyPlugins

void applyPlugins(name: string, args: any...) 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); // 依次執行指定name事件流的apply方法 }; 

觸發事件name,傳入參數args,並行的調用全部註冊在事件name上的處理函數

applyPluginsWaterfall

any applyPluginsWaterfall(name: string, init: any, args: any...) Tapable.prototype.applyPluginsWaterfall = function applyPlugins(name, init) { if(!this._plugins[name]) return init; // 若是指定事件沒有註冊事件流,則返回第2個參數(init) var args = Array.prototype.slice.call(arguments, 1); // 除name之外的其餘參數 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); // 依次執行事件流的apply()方法, 傳入的args是前執行返回值替換init初始值的參數 return current; }; 

觸發事件name,串行的調用註冊在事件name上的處理函數(先入先出),最早執行的處理函數傳入init和args,後續的處理函數傳入前一個處理函數的返回值和args,函數最終返回最後一個處理函數的返回結果

applyPluginsBailResult

any applyPluginsBailResult(name: string, args: any...) Tapable.prototype.applyPluginsBailResult = 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); // 依次執行事件流的apply()方法並取得返回值 if(typeof result !== "undefined") { // 若是返回一個不爲undefined的結果 return result; // 則中止執行並將這個結果返回。 } } }; 

觸發事件name,串行的調用註冊在事件name上的處理函數(先入先出),傳入參數args,若是其中一個處理函數返回值!== undefined,直接返回這個返回值,後續的處理函數將不被執行

applyPluginsAsync

void applyPluginsAsync( name: string, args: any..., callback: (err?: Error) -> void ) // 異步執行監聽回調的方法。這個方法是順序執行,等到第一個插件執行結束後纔會執行下一個插件 Tapable.prototype.applyPluginsAsyncSeries = Tapable.prototype.applyPluginsAsync = function applyPluginsAsync(name) { var args = Array.prototype.slice.call(arguments, 1); // 除名稱之外的其餘參數 var callback = args.pop(); // 參數數組最後一個彈出,回調函數賦給callback var plugins = this._plugins[name]; // 第一個參數爲事件名, 查找事件流數組 if(!plugins || plugins.length === 0) return callback(); // 監聽事件name爲空或沒有沒有註冊事件流,執行回調函數 var i = 0; var _this = this args.push(copyProperties(callback, function next(err) { // copyProperties將callback原型方法複製到next中並返回next if(err) return callback(err); i++; if(i >= plugins.length) { return callback(); } plugins[i].apply(_this, args); // 將下一個插件當作回調函數傳入第一個插件 })); // 利用閉包實現了一個迭代器,變量i記錄在applyPluginsAsync()方法中,並在回調中函數next( )中保持了對i的引用。 plugins[0].apply(this, args); }; function copyProperties(from, to) { // 將from原型方法複製到指定對象to中 for(var key in from) to[key] = from[key]; return to; } 

觸發事件name,串行的調用註冊在事件name上的處理函數(先入先出),假若某一個處理函數報錯,則執行傳入的callback(err),後續的處理函數將不被執行,不然最後一個處理函數調用callback。

applyPluginsAsyncSeries

applyPluginsAsyncSeries(
	name: string,
	args: any..., callback: (err: Error, result: any) -> void ) 

同applyPluginsAsync

applyPluginsAsyncWaterfall

applyPluginsAsyncWaterfall(
	name: string,
	init: any, callback: (err: Error, result: any) -> void ) 
Tapable.prototype.applyPluginsAsyncWaterfall = function applyPluginsAsyncWaterfall(name, init, callback) { if(!this._plugins[name] || this._plugins[name].length === 0) return callback(null, init); // 監聽事件name爲空或沒有沒有註冊事件流,執行回調函數 var plugins = this._plugins[name]; // 第一個參數爲事件名, 查找事件流數組 var i = 0; var _this = this var next = copyProperties(callback, function(err, value) {// copyProperties函數將callback函數加入了參數函數並返回參數函數 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); }; 

觸發事件name,串行的調用註冊在name上的處理函數(先入先出),第一個處理函數傳入參數init,後續的函數依賴於前一個函數執行回調的時候傳入的參數nextValue,假若某一個處理函數報錯,則執行傳入的callback(err),後續的處理函數將不被執行,不然最後一個處理函數調用callback(value)

applyPluginsParallel

applyPluginsParallel(
	name: string,
	args: any..., callback: (err?: Error) -> void ) Tapable.prototype.applyPluginsParallel = function applyPluginsParallel(name) { var args = Array.prototype.slice.call(arguments, 1); var callback = args.pop(); // 參數數組最後一個彈出,回調函數賦給callback if(!this._plugins[name] || this._plugins[name].length === 0) return callback();// 監聽事件name爲空或沒有沒有註冊事件流,執行回調函數 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; } }; 

觸發事件name,傳入參數args,並行的調用全部註冊在事件name上的處理函數,假若任一處理函數執行報錯,則執行callback('err'),不然當全部的處理函數都執行完的時候調用callback()

applyPluginsParallel 主要功能和最簡單的 applyPlugins 方法比較類似,不管如何都會讓全部註冊的插件運行一遍;只是相比 applyPlugins 多了一個額外的功能,它最後提供一個 callback 函數,這個 callback 的函數比較倔強,若是全部的插件x都正常執行,且最後都cb(),則會在最後執行callback裏的邏輯;不過,一旦其中某個插件運行出錯,就會調用這個callback(err),以後就算插件有錯誤也不會再調用該callback函數;

applyPluginsParallelBailResult

applyPluginsParallelBailResult(
	name: string,
	args: any..., callback: (err: Error, result: any) -> void ) 
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 currentError, currentResult; var done = []; for(var i = 0; i < plugins.length; i++) { args[args.length - 1] = (function(i) { return copyProperties(callback, function(err, result) { if(i >= currentPos) return; // ignore done.push(i); if(err || result) { currentPos = i + 1; done = done.filter(function(item) { return item <= i; }); currentError = err; currentResult = result; } if(done.length === currentPos) { callback(currentError, currentResult); currentPos = 0; } }); }(i)); plugins[i].apply(this, args); } }; 

觸發事件name,串行的執行註冊在事件name上的處理函數(先入先出),每一個處理函數必須調用callback(err, result),假若任一處理函數在調用callback(err, result)的時候,err!==undefined || result!==undefined,則callback將真正被執行,後續的處理函數則不會再被執行。

它的行爲和 applyPluginsParallel 很是類似,首先不管如何都會讓全部註冊的插件運行一遍(根據註冊的順序);爲了讓 callback 執行,其前提條件是每一個插件都須要調用 cb();但其中的 callback 只會執行一次(當傳給cb的值不是undefined/null 的時候),這一次執行順序是插件定義順序有關,而跟每一個插件中的 cb() 執行時間無關的;

hasPlugins

hasPlugins(
	name: string
)

Tapable.prototype.hasPlugins = function hasPlugins(name) { var plugins = this._plugins[name]; return plugins && plugins.length > 0; }; 

若是事件name已經被註冊了,則返回true

 

使用案例

webpack的Tapable實例之一編譯器負責編譯webpack配置對象並返回一個編譯實例。編譯實例運行時,將建立所需的捆綁包。

node_modules/webpack/lib/Compiler.js

var Tapable = require("tapable"); // 引入Tapable function Compiler() { Tapable.call(this); // 將Tapable中的this指向Compiler } Compiler.prototype = Object.create(Tapable.prototype); // Compiler 繼承 Tapable 

如今在編譯器上編寫一個插件,my-custom-plugin.js

function CustomPlugin() {} // 定義一個CustomPlugin類 CustomPlugin.prototype.apply = function(compiler) { // CustomPlugin的原型對象上添加apply入口 compiler.plugin('emit', pluginFunction); // 傳入compiler,並註冊事件流pluginFunction的事件名稱emit } 

編譯器在其生命週期的適當位置經過執行插件, node_modules/webpack/lib/Compiler.js

this.apply*("emit",options) // will fetch all plugins under 'emit' name and run them. 

參考資料

相關文章
相關標籤/搜索