Webpack源碼基礎-Tapable從使用Hook到源碼解析

當我第一次看webpack源碼的時候,會被其中跳轉頻繁的源碼所迷惑,不少地方不斷點甚至找不到頭緒,由於plugin是事件系統,沒有明確的調用棧。這一切都是由於沒有先去了解webpack的依賴庫Tapable。 Tapble是webpack在打包過程當中,控制打包在什麼階段調用Plugin的庫,是一個典型的觀察者模式的實現,但實際又比這複雜。 爲了能讓讀者最快了解Tapable的基本用法,咱們先用一個最簡單的demo代碼做爲示例,而後經過增長需求來一步步瞭解用法。前端

P.S. 因爲Tapable0.28和Tapable1.0以後的實現已經徹底不同,此處均以Tapable2.0爲準webpack

Tapable的核心功能就是控制一系列註冊事件之間的執行流控制,好比我註冊了三個事件,我能夠但願他們是併發的,或者是同步依次執行,又或者其中一個出錯後,後面的事件就不執行了,這些功能均可以經過tapable的hook實現,咱們會在後面詳細講解。web

基本用法

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咱們會在後面進行分析。promise

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._callHook.js的底部代碼被定義的,也就是createCompileDelegate的值,async

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._createCallthis.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的值最終其實由工廠類SyncHookCodeFactorycreate方法返回

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系列便可。

相關文章
相關標籤/搜索