Webpack原始碼基礎-Tapable從使用Hook到原始碼解析
當我第一次看webpack原始碼的時候,會被其中跳轉頻繁的原始碼所迷惑,很多地方不斷點甚至找不到頭緒,因為plugin是事件系統,。這一切都是因為沒有先去了解webpack的依賴庫Tapable。 Tapble是webpack在打包過程中,控制打包在什麼階段呼叫Plugin的庫,是一個典型的觀察者模式的實現,但實際又比這複雜。 為了能讓讀者最快了解Tapable的基本用法,我們先用一個最簡單的demo程式碼作為示例,然後通過增加需求來一步步瞭解用法。
P.S. 由於Tapable0.28和Tapable1.0之後的實現已經完全不一樣,此處均以Tapable2.0為準
Tapable的核心功能就是控制一系列註冊事件之間的執行流控制,比如我註冊了三個事件,我可以希望他們是併發的,或者是同步依次執行,又或者其中一個出錯後,後面的事件就不執行了,這些功能都可以通過tapable的hook實現,我們會在後面詳細講解。
基本用法
const { SyncHook } = require("tapable"); // 為了便於理解,取名為EventEmitter const EventEmitter = new SyncHook(); // tap方法用於註冊事件, 其中第一個引數僅用作註釋,增加可讀性,原始碼中並沒有用到這個變數 EventEmitter.tap('Event1', function () { console.log('Calling Event1') }); EventEmitter.tap('Event2', function () { console.log('Calling Event2') }); EventEmitter.call(); 複製程式碼
這就是最基礎的SyncHook用法,基本和前端的EventListener一樣。 除了SyncHook,Tapable還提供了一系列別的Hook
SyncBailHook, SyncWaterfallHook, SyncLoopHook, AsyncParallelHook, AsyncParallelBailHook, AsyncSeriesHook, AsyncSeriesBailHook, AsyncSeriesWaterfallHook 複製程式碼
這些Hook我們會在後面進行分析。
Tapable的“compile”
假設我們有一個需求,如果我們在兩個事件中都需要用到公用變數
const { SyncHook } = require("tapable"); // 為了便於理解,取名為EventEmitter const EventEmitter = new SyncHook(['arg1', 'arg2']); // tap方法用於註冊事件, 其中第一個引數僅用作註釋,增加可讀性,原始碼中並沒有用到這個變數 EventEmitter.tap('Event1', function (param1, param2) { console.log('Calling Event1'); console.log(param1); console.log(param2); }); EventEmitter.tap('Event2', function (param1, param2) { console.log('Calling Event2'); console.log(param1) console.log(param2) }); const arg1 = 'test1'; const arg2 = 'test2'; EventEmitter.call(arg1, arg2); // 列印結果 // Calling Event1 // test1 // test2 // Calling Event2 // test1 // test2 複製程式碼
從上面程式碼可以看出,我們在新建SyncHook例項時傳入一個數組,陣列的每一項是我們所需公共變數的形參名。然後在call方法中傳入相應數量引數。在列印結果中可以看到, 每個事件回撥函式都可以獲得正確列印變數arg1和arg2。
但是細心的讀者會疑惑,new SyncHook(['arg1', 'arg2'])
中傳入的陣列似乎沒有必要。這其實和Tapable的實現方式有關。我們嘗試在在new SyncHook()
中不傳入引數,直接在call傳入arg1和arg2。
const EventEmitter = new SyncHook(); ... ... EventEmitter.call(arg1, arg2); // 列印結果 // Calling Event1 // undefined // undefined // Calling Event2 // undefined // undefined 複製程式碼
事件回撥函式並不能獲取變數。
其實當呼叫call
方法時,Tapable內部通過字串拼接的方式,“編譯”了一個新函式,並且通過快取的方式保證這個函式只需要編譯一遍。
Tapable的xxxHook均繼承自基類Hook
,我們直接點進call
方法可以發現this.call = this._call
,而this._call
在Hook.js
的底部程式碼被定義的,也就是createCompileDelegate
的值,
Object.defineProperties(Hook.prototype, { _call: { value: createCompileDelegate("call", "sync"), configurable: true, writable: true }, _promise: { value: createCompileDelegate("promise", "promise"), configurable: true, writable: true }, _callAsync: { value: createCompileDelegate("callAsync", "async"), configurable: true, writable: true } }); 複製程式碼
createCompileDelegate
的定義如下
function createCompileDelegate(name, type) { return function lazyCompileHook(...args) { this[name] = this._createCall(type); return this[name](...args); }; } 複製程式碼
可見this._call
的值為函式lazyCompileHook
,當我們第一次呼叫的時候呼叫的時候實際是lazyCompileHook(...args)
,並且我們知道閉包變數name === 'call'
, 所以this.call
的值被替換為this._createCall(type)
。this._createCall
和this.compile
的定義如下
_createCall(type) { return this.compile({ taps: this.taps, interceptors: this.interceptors, args: this._args, type: type }); } compile(options) { throw new Error("Abstract: should be overriden"); } 複製程式碼
所以this.call
最終的返回值由衍生類自行實現,我們看一下SyncHook
的定義
const Hook = require("./Hook"); const HookCodeFactory = require("./HookCodeFactory"); class SyncHookCodeFactory extends HookCodeFactory { content({ onError, onResult, onDone, rethrowIfPossible }) { return this.callTapsSeries({ onError: (i, err) => onError(err), onDone, rethrowIfPossible }); } } const factory = new SyncHookCodeFactory(); class SyncHook extends Hook { tapAsync() { throw new Error("tapAsync is not supported on a SyncHook"); } tapPromise() { throw new Error("tapPromise is not supported on a SyncHook"); } compile(options) { factory.setup(this, options); return factory.create(options); } } 複製程式碼
可以發現this.call的值最終其實由工廠類SyncHookCodeFactory
的create
方法返回
create(options) { this.init(options); let fn; switch (this.options.type) { case "sync": // 目前我們只關心Sync fn = new Function( this.args(), '"use strict";\n' + this.header() + this.content({ onError: err => `throw ${err};\n`, onResult: result => `return ${result};\n`, onDone: () => "", rethrowIfPossible: true }) ); console.log(fn.toString()); // 此處列印fn break; case "async": fn = new Function( this.args({ after: "_callback" }), '"use strict";\n' + this.header() + this.content({ onError: err => `_callback(${err});\n`, onResult: result => `_callback(null, ${result});\n`, onDone: () => "_callback();\n" }) ); break; case "promise": let code = ""; code += '"use strict";\n'; code += "return new Promise((_resolve, _reject) => {\n"; code += "var _sync = true;\n"; code += this.header(); code += this.content({ onError: err => { let code = ""; code += "if(_sync)\n"; code += `_resolve(Promise.resolve().then(() => { throw ${err}; }));\n`; code += "else\n"; code += `_reject(${err});\n`; return code; }, onResult: result => `_resolve(${result});\n`, onDone: () => "_resolve();\n" }); code += "_sync = false;\n"; code += "});\n"; fn = new Function(this.args(), code); break; } this.deinit(); return fn; } 複製程式碼
這裡利用Function的建構函式形式,並且傳入字串拼接生產函式,這在我們平時開發中用得比較少,我們直接列印一下最終返回的fn,也就是this.call的實際值。
function anonymous(/*``*/) { "use strict"; var _context; // _x為儲存註冊回撥函式的陣列 var _x = this._x; var _fn0 = _x[0]; _fn0(); var _fn1 = _x[1]; _fn1(); } 複製程式碼
到這裡為止一目瞭然,我們可以看到我們的註冊回撥是怎樣在this.call方法中一步步執行的。
至於為什麼要用這種曲折的方法實現this.call
,我們在文末在進行介紹,
接下來我們就通過列印fn
來看看Tapable的一系列Hook函式的實現。
Tapable的xxxHook方法解析
Tapable有一系列Hook方法,但是這麼多的Hook方法都是無非是為了控制註冊事件的執行順序 以及異常處理 。
Sync
最簡單的SyncHook
前面已經講過,我們從SyncBailHook
開始看。
SyncBailHook
const { SyncBailHook } = require("tapable"); const EventEmitter = new SyncBailHook(); EventEmitter.tap('Event1', function () { console.log('Calling Event1') }); EventEmitter.tap('Event2', function () { console.log('Calling Event2') }); EventEmitter.call(); // 列印fn function anonymous(/*``*/) { "use strict"; var _context; var _x = this._x; var _fn0 = _x[0]; var _result0 = _fn0(); if (_result0 !== undefined) { return _result0; } else { var _fn1 = _x[1]; var _result1 = _fn1(); if (_result1 !== undefined) { return _result1; } else { } } } 複製程式碼
通過列印fn,我們可以輕易的看出,SyncBailHook提供了中止註冊函式執行的機制,只要在某個註冊回撥中返回一個非undefined的值,執行就會中止。 Tap這個單詞除了輕拍的意思,還有水龍頭的意思,相信取名為Tapable的意思就是表示這個是一個事件流控制庫,而Bail有保釋和舀水的意思,很容易明白這是帶中止機制的一個Hook。
SyncWaterfallHook
const { SyncWaterfallHook } = require("tapable"); const EventEmitter = new SyncWaterfallHook(['arg1']); EventEmitter.tap('Event1', function () { console.log('Calling Event1') return 'Event1returnValue' }); EventEmitter.tap('Event2', function () { console.log('Calling Event2') }); EventEmitter.call(); // 列印fn function anonymous(arg1) { "use strict"; var _context; var _x = this._x; var _fn0 = _x[0]; var _result0 = _fn0(arg1); if (_result0 !== undefined) { arg1 = _result0; } var _fn1 = _x[1]; var _result1 = _fn1(arg1); if (_result1 !== undefined) { arg1 = _result1; } return arg1; } 複製程式碼
可以看出SyncWaterfallHook
就是將上一個事件註冊回撥的返回值作為下一個註冊函式的引數,這就要求在new SyncWaterfallHook(['arg1']);
需要且只能傳入一個形參。
SyncLoopHook
const { SyncLoopHook } = require("tapable"); const EventEmitter = new SyncLoopHook(['arg1']); let counts = 5; EventEmitter.tap('Event1', function () { console.log('Calling Event1'); counts--; console.log(counts); if (counts <= 0) { return; } return counts; }); EventEmitter.tap('Event2', function () { console.log('Calling Event2') }); EventEmitter.call(); // 列印fn function anonymous(arg1) { "use strict"; var _context; var _x = this._x; var _loop; do { _loop = false; var _fn0 = _x[0]; var _result0 = _fn0(arg1); if (_result0 !== undefined) { _loop = true; } else { var _fn1 = _x[1]; var _result1 = _fn1(arg1); if (_result1 !== undefined) { _loop = true; } else { if (!_loop) { } } } } while (_loop); } // 列印結果 // Calling Event1 // 4 // Calling Event1 // 3 // Calling Event1 // 2 // Calling Event1 // 1 // Calling Event1 // 0 // Calling Event2 複製程式碼
SyncLoopHook
只有當上一個註冊事件函式返回undefined的時候才會執行下一個註冊函式,否則就不斷重複呼叫。
Async
Async系列的Hook在每個函式提供了next作為回撥函式,用於控制非同步流程
AsyncSeriesHook
Series有順序的意思,這個Hook用於按順序執行非同步函式。
const { AsyncSeriesHook } = require("tapable"); const EventEmitter = new AsyncSeriesHook(); // 我們從將tap改為tapAsync,專門用於非同步處理,並且只有tapAsync提供了next的回撥函式 EventEmitter.tapAsync('Event1', function (next) { console.log('Calling Event1'); setTimeout( () => { console.log('AsyncCall in Event1') next() }, 1000, ) }); EventEmitter.tapAsync('Event2', function (next) { console.log('Calling Event2'); next() }); //此處傳入最終完成的回撥 EventEmitter.callAsync((err) => { if (err) { console.log(err); return; } console.log('Async Series Call Done') }); // 列印fn function anonymous(_callback) { "use strict"; var _context; var _x = this._x; var _fn0 = _x[0]; _fn0(_err0 => { if (_err0) { _callback(_err0); } else { var _fn1 = _x[1]; _fn1(_err1 => { if (_err1) { _callback(_err1); } else { _callback(); } }); } }); } // 列印結果 // Calling Event1 // AsyncCall in Event1 // Calling Event2 // Async Series Call Done 複製程式碼
從列印結果可以發現,兩個事件之前是序列的,並且next中可以傳入err引數,當傳入err,直接中斷非同步,並且將err傳入我們在call方法傳入的完成回撥函式中。
AsyncParallelHook
const { AsyncParallelHook } = require("tapable"); const EventEmitter = new AsyncParallelHook(); // 我們從將tap改為tapAsync,專門用於非同步處理,並且只有tapAsync提供了next的回撥函式 EventEmitter.tapAsync('Event1', function (next) { console.log('Calling Event1'); setTimeout( () => { console.log('AsyncCall in Event1') next() }, 1000, ) }); EventEmitter.tapAsync('Event2', function (next) { console.log('Calling Event2'); next() }); //此處傳入最終完成的回撥 EventEmitter.callAsync((err) => { if (err) { console.log(err); return; } console.log('Async Series Call Done') }); // 列印fn function anonymous(_callback) { "use strict"; var _context; var _x = this._x; do { var _counter = 2; var _done = () => { _callback(); }; if (_counter <= 0) break; var _fn0 = _x[0]; _fn0(_err0 => { // 呼叫這個函式的時間不能確定,有可能已經執行了接下來的幾個註冊函式 if (_err0) { // 如果還沒執行所有註冊函式,終止 if (_counter > 0) { _callback(_err0); _counter = 0; } } else { // 同樣,由於函式實際呼叫時間無法確定,需要檢查是否已經執行完畢, if (--_counter === 0) _done(); } }); // 執行下一個註冊回撥之前,檢查_counter是否被重置等,如果重置說明某些地方返回err,直接終止。 if (_counter <= 0) break; var _fn1 = _x[1]; _fn1(_err1 => { if (_err1) { if (_counter > 0) { _callback(_err1); _counter = 0; } } else { if (--_counter === 0) _done(); } }); } while (false); } // 列印結果 // Calling Event1 // Calling Event2 // AsyncCall in Event1 // Async Series Call Done 複製程式碼
從列印結果看出Event2的呼叫在AsyncCall in Event1之前,說明非同步事件是併發的。
剩下的AsyncParallelBailHook, AsyncSeriesBailHook, AsyncSeriesWaterfallHook
其實大同小異,類比Sync系列即可。