阅读 326

Webpack核心Tapable从应用到源码之三(源码)

前言

本篇正式进入到源码解读。

整体看,Tapable 的源码并不复杂,核心就两个类 HookHookCodeFactory

先以 SyncHook 为例,看一下它的内部实现,逐渐引入到 HookHookCodeFactory 的内部实现逻辑上。

最后说一下两个帮助类 HookMapMultiHook

源码

SyncHook 为例,看一下它的实现。

SyncHook

此类的实现在文件 tapable/lib/SyncHook.js 中。

const Hook = require("./Hook");
const HookCodeFactory = require("./HookCodeFactory");

class SyncHookCodeFactory extends HookCodeFactory {
	content({ onError, onDone, rethrowIfPossible }) {
		return this.callTapsSeries({
			onError: (i, err) => onError(err),
			onDone,
			rethrowIfPossible
		});
	}
}

const factory = new SyncHookCodeFactory();

const TAP_ASYNC = () => {
	throw new Error("tapAsync is not supported on a SyncHook");
};

const TAP_PROMISE = () => {
	throw new Error("tapPromise is not supported on a SyncHook");
};

const COMPILE = function(options) {
	factory.setup(this, options);
	return factory.create(options);
};

function SyncHook(args = [], name = undefined) {
	const hook = new Hook(args, name);
	hook.constructor = SyncHook;
	hook.tapAsync = TAP_ASYNC;
	hook.tapPromise = TAP_PROMISE;
	hook.compile = COMPILE;
	return hook;
}

SyncHook.prototype = null;

module.exports = SyncHook;
复制代码

整体逻辑

  1. 先定义类 SyncHookCodeFactory 并继承 HookCodeFactoryHookCodeFactory 是生成可执行代码的类
  2. 接着实例化 SyncHookCodeFactory
  3. 然后定义 TAP_ASYNCTAP_PROMISECOMPILE 方法。因为 SyncHook 是同步的,没有异步订阅的功能,所以使用 tapAsynctapPromise 订阅时,抛出错误
  4. 定义同步 SyncHook 函数,并导出

SyncHook 方法内部逻辑

  1. 通过 new Hook 实例化 Hook
  2. 然后修改实例化对象的构造函数对象
  3. 重写实例化对象的 tapAsynctapPromise 方法
  4. 重写实例化对象的 compile 方法
  5. 返回实例化对象

我们先忽略 HookHookCodeFactory 类的具体实现,整个文件的逻辑就是一个完整的套路。

其他的 Hook 也是按照找个套路实现,差别只在继承 HookCodeFactory 的类上以及它们各自的 content 方法的实现。

比如 AsyncSeriesLoopHook 的实现如下: 代码在文件 tapable/lib/AsyncSeriesLoopHook.js

const Hook = require("./Hook");
const HookCodeFactory = require("./HookCodeFactory");

class AsyncSeriesLoopHookCodeFactory extends HookCodeFactory {
	content({ onError, onDone }) {
		return this.callTapsLooping({
			onError: (i, err, next, doneBreak) => onError(err) + doneBreak(true),
			onDone
		});
	}
}

const factory = new AsyncSeriesLoopHookCodeFactory();

const COMPILE = function(options) {
	factory.setup(this, options);
	return factory.create(options);
};

function AsyncSeriesLoopHook(args = [], name = undefined) {
	const hook = new Hook(args, name);
	hook.constructor = AsyncSeriesLoopHook;
	hook.compile = COMPILE;
	hook._call = undefined;
	hook.call = undefined;
	return hook;
}

AsyncSeriesLoopHook.prototype = null;

module.exports = AsyncSeriesLoopHook;
复制代码

AsyncSeriesLoopHook 是异步方法,所以重置了它实例化对象上的 call_call 方法即不能使用 call 方法进行调用。

从源码确定两点

查看源码可以确定两点:

  1. 同步 Hook 是不能通过 tapAsynctapPromise 进行订阅的,因为这两个方法被重置为:
const TAP_ASYNC = () => {
	throw new Error("tapAsync is not supported on a SyncHook");
};

const TAP_PROMISE = () => {
	throw new Error("tapPromise is not supported on a SyncHook");
};
复制代码
  1. 异步 Hook 是不能通过 call 方法进行调用的,因为它被重置为:
hook._call = undefined;
hook.call = undefined;
复制代码

Hook 类

此类的定义在文件 tapable/lib/Hook.js 中。

const CALL_DELEGATE = function(...args) {
	this.call = this._createCall("sync");
	return this.call(...args);
};
const CALL_ASYNC_DELEGATE = function(...args) {
	this.callAsync = this._createCall("async");
	return this.callAsync(...args);
};
const PROMISE_DELEGATE = function(...args) {
	this.promise = this._createCall("promise");
	return this.promise(...args);
};

class Hook {
	constructor(args = [], name = undefined) {
		this._args = args;
		this.name = name;
		this.taps = [];
		this.interceptors = [];
		this._call = CALL_DELEGATE;
		this.call = CALL_DELEGATE;
		this._callAsync = CALL_ASYNC_DELEGATE;
		this.callAsync = CALL_ASYNC_DELEGATE;
		this._promise = PROMISE_DELEGATE;
		this.promise = PROMISE_DELEGATE;
		this._x = undefined;

		this.compile = this.compile;
		this.tap = this.tap;
		this.tapAsync = this.tapAsync;
		this.tapPromise = this.tapPromise;
	}

	compile(options) {
		throw new Error("Abstract: should be overridden");
	}

	_createCall(type) {
		return this.compile({
			taps: this.taps,
			interceptors: this.interceptors,
			args: this._args,
			type: type
		});
	}

	_tap(type, options, fn) {
		if (typeof options === "string") {
			options = {
				name: options.trim()
			};
		} else if (typeof options !== "object" || options === null) {
			throw new Error("Invalid tap options");
		}
		if (typeof options.name !== "string" || options.name === "") {
			throw new Error("Missing name for tap");
		}
		if (typeof options.context !== "undefined") {
			deprecateContext();
		}
		options = Object.assign({ type, fn }, options);
		options = this._runRegisterInterceptors(options);
		this._insert(options);
	}

	tap(options, fn) {
		this._tap("sync", options, fn);
	}

	tapAsync(options, fn) {
		this._tap("async", options, fn);
	}

	tapPromise(options, fn) {
		this._tap("promise", options, fn);
	}

	_runRegisterInterceptors(options) {
		for (const interceptor of this.interceptors) {
			if (interceptor.register) {
				const newOptions = interceptor.register(options);
				if (newOptions !== undefined) {
					options = newOptions;
				}
			}
		}
		return options;
	}

	withOptions(options) {
		const mergeOptions = opt =>
			Object.assign({}, options, typeof opt === "string" ? { name: opt } : opt);

		return {
			name: this.name,
			tap: (opt, fn) => this.tap(mergeOptions(opt), fn),
			tapAsync: (opt, fn) => this.tapAsync(mergeOptions(opt), fn),
			tapPromise: (opt, fn) => this.tapPromise(mergeOptions(opt), fn),
			intercept: interceptor => this.intercept(interceptor),
			isUsed: () => this.isUsed(),
			withOptions: opt => this.withOptions(mergeOptions(opt))
		};
	}

	isUsed() {
		return this.taps.length > 0 || this.interceptors.length > 0;
	}

	intercept(interceptor) {
		this._resetCompilation();
		this.interceptors.push(Object.assign({}, interceptor));
		if (interceptor.register) {
			for (let i = 0; i < this.taps.length; i++) {
				this.taps[i] = interceptor.register(this.taps[i]);
			}
		}
	}

	_resetCompilation() {
		this.call = this._call;
		this.callAsync = this._callAsync;
		this.promise = this._promise;
	}

	_insert(item) {
		this._resetCompilation();
		let before;
		if (typeof item.before === "string") {
			before = new Set([item.before]);
		} else if (Array.isArray(item.before)) {
			before = new Set(item.before);
		}
		let stage = 0;
		if (typeof item.stage === "number") {
			stage = item.stage;
		}
		let i = this.taps.length;
		while (i > 0) {
			i--;
			const x = this.taps[i];
			this.taps[i + 1] = x;
			const xStage = x.stage || 0;
			if (before) {
				if (before.has(x.name)) {
					before.delete(x.name);
					continue;
				}
				if (before.size > 0) {
					continue;
				}
			}
			if (xStage > stage) {
				continue;
			}
			i++;
			break;
		}
		this.taps[i] = item;
	}
}
复制代码

在构造函数中,定义了 taptapAsynctapPromise 方法,这是那个方法是我们订阅函数时所使用的方法。

这三个方法在原型链上都调用了 this._tap 的私有方法,唯一不同的是第一个参数 syncasyncpromise 代表了不同的订阅函数类型。

_tap 方法

_tap(type, options, fn) {
	if (typeof options === "string") {
		options = {
			name: options.trim()
		};
	} else if (typeof options !== "object" || options === null) {
		throw new Error("Invalid tap options");
	}
	if (typeof options.name !== "string" || options.name === "") {
		throw new Error("Missing name for tap");
	}
	if (typeof options.context !== "undefined") {
		deprecateContext();
	}
	options = Object.assign({ type, fn }, options);
	options = this._runRegisterInterceptors(options);
	this._insert(options);
}
复制代码

私有方法 _tap 主要功能是序列化 options 并添加到 taps 数组中。

实现逻辑:

  1. 检查参数 options 的类型,options 最终还是一个对象,而且必须要有 name 属性
  2. 如果 optionscontext 属性,会调用 deprecateContext 方法,提示即将废弃 context 属性
  3. 调用 assign 方法合并 options,这样它就有了 nametypefn 属性
  4. 调用私有方法 _runRegisterInterceptors 修改 options
  5. 调用私有方法 _insert 调整订阅函数的顺序并存放在 taps 数组中

_runRegisterInterceptors 方法

_runRegisterInterceptors(options) {
	for (const interceptor of this.interceptors) {
		if (interceptor.register) {
			const newOptions = interceptor.register(options);
			if (newOptions !== undefined) {
				options = newOptions;
			}
		}
	}
	return options;
}
复制代码

私有方法 _runRegisterInterceptors 主要功能是修改 options 对象。

实现逻辑

  • 遍历所有拦截器,如果有 register 属性方法,就调用它并传递原始的 options 作为参数,如果返回值不是 undefined 就修改 options,最后一个拦截器的 register 修改后的 options 就是最后最终的 options

所有拦截器的 register 属性方法有修改订阅函数 Tap 对象的能力。

_insert 方法

_insert(item) {
	this._resetCompilation();
	let before;
	if (typeof item.before === "string") {
		before = new Set([item.before]);
	} else if (Array.isArray(item.before)) {
		before = new Set(item.before);
	}
	let stage = 0;
	if (typeof item.stage === "number") {
		stage = item.stage;
	}
	let i = this.taps.length;
	while (i > 0) {
		i--;
		const x = this.taps[i];
		this.taps[i + 1] = x;
		const xStage = x.stage || 0;
		if (before) {
			if (before.has(x.name)) {
				before.delete(x.name);
				continue;
			}
			if (before.size > 0) {
				continue;
			}
		}
		if (xStage > stage) {
			continue;
		}
		i++;
		break;
	}
	this.taps[i] = item;
}
复制代码

私有方法 _run 主要功能是根据 beforestage 的值调整订阅函数的顺序,最终存储在 taps 数组中。

实现逻辑

  1. 先把 before 转换为 Set
  2. 检查 stage 如果是数字类型,就赋值给变量 stage
  3. taps 末尾开始遍历
    1. 先让 i--
    2. 获取 i 位置的值 x
    3. 在把 x 赋值到 i + 1 的位置,这样就把 x 向后移动了一个位置
    4. 获取 xstage
    5. 如果有 before
      1. 判断如果 xnamebefore 中存在,也就是当前待插入的 item 要放在 x 之前,就 continue ,就再执行 while
    6. 如果没有 before,判断 xstage 是否大于 itemstage,如果大于就 continue
    7. i++ ,结束循环,这样就找到了应该插入的索引位置
  4. item 存放在 i 的位置上

用图来表示一下上面的逻辑: image image image image image

call、callAsync 和 promise 方法

这三个方法都是调用了 _createCall 私有方法,然后把返回值重新赋值给相应的方法。

比如 call 方法

this.call = CALL_DELEGATE;
复制代码
const CALL_DELEGATE = function(...args) {
	this.call = this._createCall("sync");
	return this.call(...args);
};
复制代码

_createCall 方法

_createCall(type) {
	return this.compile({
		taps: this.taps,
		interceptors: this.interceptors,
		args: this._args,
		type: type
	});
}
复制代码

此方法调用了 compile 方法。

回到文章的开头,每个 Hook 类 在实例化后都会重写 compile 方法,也就是进入到了 HookCodeFactory 的逻辑。

HookCodeFactory 类

示例代码

我们以下面为示例,来看一下内部是怎么进入到 HookCodeFactory 逻辑的。

const {
  SyncHook
} = require('tapable');

const hook = new SyncHook(['name', 'age']);

hook.tap({
  name: 'js'
}, (name, age) => {
  console.log('js', name, age);
});
hook.tap({
  name: 'css'
}, (name, age) => {
  console.log('css', name, age);
});
hook.tap({
  name: 'node'
}, (name, age) => {
  console.log('node', name, age);
});

hook.call('naonao', 2);
复制代码

在上篇文章中已经对 hook.tap 的实现,进行了源码解析。

我们看一下 hook.call 的执行。

class Hook 的构造函数中有:

this.call 定义

this.call = CALL_DELEGATE;
复制代码

CALL_DELEGATE 定义

const CALL_DELEGATE = function(...args) {
	this.call = this._createCall("sync");
	return this.call(...args);
};
复制代码

_createCall 定义:

_createCall(type) {
	return this.compile({
		taps: this.taps,
		interceptors: this.interceptors,
		args: this._args,
		type: type
	});
}
复制代码

说明几点

这个地方说明几点:

  1. this.taps 存储的内容类似如下:
[
    {type: 'sync', fn: f, name: 'js'},
    {type: 'sync', fn: f, name: 'css'},
    {type: 'sync', fn: f, name: 'node'}
]
复制代码
  1. this.interceptors 存储的内容是拦截器相关的对象,也是一个数组
  2. this._args 是类实例化时传入的 args,类似如下:
['name', 'age']
复制代码
  1. typesyncasyncpromise 中的一个

this.compile 定义

this.compile 方法是每个 Hook 在实例化后被重写的。

const COMPILE = function(options) {
	factory.setup(this, options);
	return factory.create(options);
};

function SyncHook(args = [], name = undefined) {
	const hook = new Hook(args, name);
	// 省略部分代码...
	hook.compile = COMPILE;
	return hook;
}
复制代码

COMPILE 内部逻辑

  1. 调用 setup 方法
  2. 调用 create 方法并返回

create 方法的返回结果最终会赋值给 CALL_DELEGATE 方法的 this.call, 再执行 this.call

以上就把 hook.call 的执行逻辑梳理完毕了。

下面我们来看一下 COMPILE 方法内部执行的 setupcreate 方法。

setup

setup 方法在 HookCodeFactory 中定义如下:

setup(instance, options) {
	instance._x = options.taps.map(t => t.fn);
}
复制代码

它对 taps 进行 map 处理,把 fn 抽离出来,赋值给当前 Hook 实例的 _x 属性。

create

create 方法在 HookCodeFactory 中定义如下:

create(options) {
	this.init(options);
	let fn;
	switch (this.options.type) {
		case "sync":
			fn = new Function(
				this.args(),
				'"use strict";\n' +
					this.header() +
					this.contentWithInterceptors({
						onError: err => `throw ${err};\n`,
						onResult: result => `return ${result};\n`,
						resultReturns: true,
						onDone: () => "",
						rethrowIfPossible: true
					})
			);
			break;
		case "async":
			fn = new Function(
				this.args({
					after: "_callback"
				}),
				'"use strict";\n' +
					this.header() +
					this.contentWithInterceptors({
						onError: err => `_callback(${err});\n`,
						onResult: result => `_callback(null, ${result});\n`,
						onDone: () => "_callback();\n"
					})
			);
			break;
		case "promise":
			let errorHelperUsed = false;
			const content = this.contentWithInterceptors({
				onError: err => {
					errorHelperUsed = true;
					return `_error(${err});\n`;
				},
				onResult: result => `_resolve(${result});\n`,
				onDone: () => "_resolve();\n"
			});
			let code = "";
			code += '"use strict";\n';
			code += this.header();
			code += "return new Promise((function(_resolve, _reject) {\n";
			if (errorHelperUsed) {
				code += "var _sync = true;\n";
				code += "function _error(_err) {\n";
				code += "if(_sync)\n";
				code +=
					"_resolve(Promise.resolve().then((function() { throw _err; })));\n";
				code += "else\n";
				code += "_reject(_err);\n";
				code += "};\n";
			}
			code += content;
			if (errorHelperUsed) {
				code += "_sync = false;\n";
			}
			code += "}));\n";
			fn = new Function(this.args(), code);
			break;
	}
	this.deinit();
	return fn;
}
复制代码

它是根据 type 不同,拼接出不同的代码字符串并且通过 new Function 创建一个函数,赋值给 fn,最后返回 fn

所以在之前,打印出 hook.call 方法是完全能看出订阅函数的执行顺序和流程的。

sync

先看 typesynccase 分支:

case "sync":
	fn = new Function(
		this.args(),
		'"use strict";\n' +
			this.header() +
			this.contentWithInterceptors({
				onError: err => `throw ${err};\n`,
				onResult: result => `return ${result};\n`,
				resultReturns: true,
				onDone: () => "",
				rethrowIfPossible: true
			})
	);
	break;
复制代码

主要逻辑如下:

  1. 调用 this.args 方法,获取函数参数
  2. "use strict" 开始就是拼接函数主体的字符串

this.args 方法

args({ before, after } = {}) {
	let allArgs = this._args;
	if (before) allArgs = [before].concat(allArgs);
	if (after) allArgs = allArgs.concat(after);
	if (allArgs.length === 0) {
		return "";
	} else {
		return allArgs.join(", ");
	}
}
复制代码

this._args 数组按照 **, ** 拼接为字符串

this.header 方法

header() {
	let code = "";
	if (this.needContext()) {
		code += "var _context = {};\n";
	} else {
		code += "var _context;\n";
	}
	code += "var _x = this._x;\n";
	if (this.options.interceptors.length > 0) {
		code += "var _taps = this.taps;\n";
		code += "var _interceptors = this.interceptors;\n";
	}
	return code;
}
复制代码
主要逻辑
  1. 调用 this.needContext 方法,判断是否需要 context 对象。如果需要就定义 context 是一个空对象。判断的根据就是 this.taps 中是否有某个成员配置了 context: true 也即类似 { type: 'sync', fn: f, name: 'js', context: true }
  2. 定义缓存 _xthis._x,也就是订阅函数 fn 组成的数组
  3. 判断是否有拦截器,如果有定义缓存响应的 _taps_interceptors
  4. 返回拼接的字符串 code

this.contentWithInterceptors 方法

contentWithInterceptors(options) {
	if (this.options.interceptors.length > 0) {
		const onError = options.onError;
		const onResult = options.onResult;
		const onDone = options.onDone;
		let code = "";
		for (let i = 0; i < this.options.interceptors.length; i++) {
			const interceptor = this.options.interceptors[i];
			if (interceptor.call) {
				code += `${this.getInterceptor(i)}.call(${this.args({
					before: interceptor.context ? "_context" : undefined
				})});\n`;
			}
		}
		code += this.content(
			Object.assign(options, {
				onError:
					onError &&
					(err => {
						let code = "";
						for (let i = 0; i < this.options.interceptors.length; i++) {
							const interceptor = this.options.interceptors[i];
							if (interceptor.error) {
								code += `${this.getInterceptor(i)}.error(${err});\n`;
							}
						}
						code += onError(err);
						return code;
					}),
				onResult:
					onResult &&
					(result => {
						let code = "";
						for (let i = 0; i < this.options.interceptors.length; i++) {
							const interceptor = this.options.interceptors[i];
							if (interceptor.result) {
								code += `${this.getInterceptor(i)}.result(${result});\n`;
							}
						}
						code += onResult(result);
						return code;
					}),
				onDone:
					onDone &&
					(() => {
						let code = "";
						for (let i = 0; i < this.options.interceptors.length; i++) {
							const interceptor = this.options.interceptors[i];
							if (interceptor.done) {
								code += `${this.getInterceptor(i)}.done();\n`;
							}
						}
						code += onDone();
						return code;
					})
			})
		);
		return code;
	} else {
		return this.content(options);
	}
}
复制代码
  1. 先看一下没有没有拦截器的逻辑即 else 逻辑,直接返回了 this.content 的结果。

this.content 是定义在

class SyncHookCodeFactory extends HookCodeFactory {
	content({ onError, onDone, rethrowIfPossible }) {
		return this.callTapsSeries({
			onError: (i, err) => onError(err),
			onDone,
			rethrowIfPossible
		});
	}
}
复制代码

可以看出它是调用了 this.callTapsSeries 并返回了其结果。

  1. 再看一下有拦截器的情况:
    1. for 循环所有的拦截器,如果配置了 call 属性方法,就拼接出每个拦截器执行 call 方法的字符串
    2. 还是执行 this.content 方法,只是参数是用原始的 options 和 拦截器上每一个 errorresultdone 可执行代码字符串拼接后的合并结果

callTapsSeries

callTapsSeries({
	onError,
	onResult,
	resultReturns,
	onDone,
	doneReturns,
	rethrowIfPossible
}) {
	if (this.options.taps.length === 0) return onDone();
	const firstAsync = this.options.taps.findIndex(t => t.type !== "sync");
	const somethingReturns = resultReturns || doneReturns;
	let code = "";
	let current = onDone;
	let unrollCounter = 0;
	for (let j = this.options.taps.length - 1; j >= 0; j--) {
		const i = j;
		const unroll =
			current !== onDone &&
			(this.options.taps[i].type !== "sync" || unrollCounter++ > 20);
		if (unroll) {
			unrollCounter = 0;
			code += `function _next${i}() {\n`;
			code += current();
			code += `}\n`;
			current = () => `${somethingReturns ? "return " : ""}_next${i}();\n`;
		}
		const done = current;
		const doneBreak = skipDone => {
			if (skipDone) return "";
			return onDone();
		};
		const content = this.callTap(i, {
			onError: error => onError(i, error, done, doneBreak),
			onResult:
				onResult &&
				(result => {
					return onResult(i, result, done, doneBreak);
				}),
			onDone: !onResult && done,
			rethrowIfPossible:
				rethrowIfPossible && (firstAsync < 0 || i < firstAsync)
		});
		current = () => content;
	}
	code += current();
	return code;
}
复制代码

主要功能是倒序遍历 taps,先忽略异步的情况。

重点看 this.callTap 部分,调用它把返回结果赋值给 content,然后重置 current() => content; 这样利用闭包的特性,就缓存了上一次拼接出的字符串。

而每次循环时,又会把 current 赋值给 done ,在调用 this.callTap 内部会执行 done 方法,把它的结果拼接在相应的位置。

最后返回拼接的字符串 code

callTap

callTap(tapIndex, { onError, onResult, onDone, rethrowIfPossible }) {
	let code = "";
	let hasTapCached = false;
	for (let i = 0; i < this.options.interceptors.length; i++) {
		const interceptor = this.options.interceptors[i];
		if (interceptor.tap) {
			if (!hasTapCached) {
				code += `var _tap${tapIndex} = ${this.getTap(tapIndex)};\n`;
				hasTapCached = true;
			}
			code += `${this.getInterceptor(i)}.tap(${
				interceptor.context ? "_context, " : ""
			}_tap${tapIndex});\n`;
		}
	}
	code += `var _fn${tapIndex} = ${this.getTapFn(tapIndex)};\n`;
	const tap = this.options.taps[tapIndex];
	switch (tap.type) {
		case "sync":
			if (!rethrowIfPossible) {
				code += `var _hasError${tapIndex} = false;\n`;
				code += "try {\n";
			}
			if (onResult) {
				code += `var _result${tapIndex} = _fn${tapIndex}(${this.args({
					before: tap.context ? "_context" : undefined
				})});\n`;
			} else {
				code += `_fn${tapIndex}(${this.args({
					before: tap.context ? "_context" : undefined
				})});\n`;
			}
			if (!rethrowIfPossible) {
				code += "} catch(_err) {\n";
				code += `_hasError${tapIndex} = true;\n`;
				code += onError("_err");
				code += "}\n";
				code += `if(!_hasError${tapIndex}) {\n`;
			}
			if (onResult) {
				code += onResult(`_result${tapIndex}`);
			}
			if (onDone) {
				code += onDone();
			}
			if (!rethrowIfPossible) {
				code += "}\n";
			}
			break;
		case "async":
			let cbCode = "";
			if (onResult)
				cbCode += `(function(_err${tapIndex}, _result${tapIndex}) {\n`;
			else cbCode += `(function(_err${tapIndex}) {\n`;
			cbCode += `if(_err${tapIndex}) {\n`;
			cbCode += onError(`_err${tapIndex}`);
			cbCode += "} else {\n";
			if (onResult) {
				cbCode += onResult(`_result${tapIndex}`);
			}
			if (onDone) {
				cbCode += onDone();
			}
			cbCode += "}\n";
			cbCode += "})";
			code += `_fn${tapIndex}(${this.args({
				before: tap.context ? "_context" : undefined,
				after: cbCode
			})});\n`;
			break;
		case "promise":
			code += `var _hasResult${tapIndex} = false;\n`;
			code += `var _promise${tapIndex} = _fn${tapIndex}(${this.args({
				before: tap.context ? "_context" : undefined
			})});\n`;
			code += `if (!_promise${tapIndex} || !_promise${tapIndex}.then)\n`;
			code += `  throw new Error('Tap function (tapPromise) did not return promise (returned ' + _promise${tapIndex} + ')');\n`;
			code += `_promise${tapIndex}.then((function(_result${tapIndex}) {\n`;
			code += `_hasResult${tapIndex} = true;\n`;
			if (onResult) {
				code += onResult(`_result${tapIndex}`);
			}
			if (onDone) {
				code += onDone();
			}
			code += `}), function(_err${tapIndex}) {\n`;
			code += `if(_hasResult${tapIndex}) throw _err${tapIndex};\n`;
			code += onError(`_err${tapIndex}`);
			code += "});\n";
			break;
	}
	return code;
}
复制代码

开头是遍历拦截器,如果有 tap 属性方法,就拼接出它的执行字符串代码。

所以每个订阅函数执行前都会先执行拦截器的 tap 方法。

接着是定义 var _fn${tapIndex} ,其实就是 var _fn0 = _x[0] 这种形式。

然后判断当前的 taptype 类型,进入不同的分支。

这里看一下 sync 分支。

  1. 如果 rethrowIfPossiblefalse 就拼接 ** _hasError** 和 try
  2. 根据 onResult 的情况,拼接出是否需要缓存订阅函数的执行结果为 __result,因为有些 Hook 需要根据结果决定是否继续向下执行
  3. 再次根据 rethrowIfPossible 的情况,拼接 catch 部分,这样 try...catch 就完整了,第2步就是 try 的内容
  4. 再根据 onResult 的情况,把缓存的执行结果当参数传递,拼接 if...else 内容。比如 SyncBailHookonResult 是:
class SyncBailHookCodeFactory extends HookCodeFactory {
	content({ onError, onResult, resultReturns, onDone, rethrowIfPossible }) {
		return this.callTapsSeries({
			onError: (i, err) => onError(err),
			onResult: (i, result, next) =>
				`if(${result} !== undefined) {\n${onResult(
					result
				)};\n} else {\n${next()}}\n`,
			resultReturns,
			onDone,
			rethrowIfPossible
		});
	}
}
复制代码

从这里的 onResult 就可以拼接出决定执行结果不是 undefined 时就不会执行 next 也就是不会向下执行的字符串代码了。 5. 执行 onDone 就是 callTapsSeries 方法中的 current ,它缓存的是上一次订阅函数执行拼接的字符串代码 6. 最后根据 rethrowIfPossible 闭合代码块

这就是整个的拼接流程。

拼接的字符串代码,最终通过 new Function 创建出一个新的函数并赋值给 call

tapAsyncpromise 也是按照这个流程进行拼接的,只是具体拼接的代码有所区别。详细的参考分支里的 asyncpromise 吧。

HookMap

HookMap 类就是根据字符串映射到一个 Hook 上。内部就是根据 key 存储在一个 Map 对象中,用的时候根据具体的 key 值就能获取到对应的 Hook

class HookMap {
	constructor(factory, name = undefined) {
		this._map = new Map();
		this.name = name;
		this._factory = factory;
		this._interceptors = [];
	}

	get(key) {
		return this._map.get(key);
	}

	for(key) {
		const hook = this.get(key);
		if (hook !== undefined) {
			return hook;
		}
		let newHook = this._factory(key);
		const interceptors = this._interceptors;
		for (let i = 0; i < interceptors.length; i++) {
			newHook = interceptors[i].factory(key, newHook);
		}
		this._map.set(key, newHook);
		return newHook;
	}

	intercept(interceptor) {
		this._interceptors.push(
			Object.assign(
				{
					factory: defaultFactory
				},
				interceptor
			)
		);
	}
}
复制代码

其实就是 Mapsetget 的操作。

MultiHook

MultiHook 就是定义很多 Hook 存放在数组中,当使用 taptapAsynctapPromise 时,就遍历数组,并调用每个成员相应的 taptapAsynctapPromise 方法。

这就是定义一次订阅函数,可以分发到很多 Hook 上。

class MultiHook {
	constructor(hooks, name = undefined) {
		this.hooks = hooks;
		this.name = name;
	}

	tap(options, fn) {
		for (const hook of this.hooks) {
			hook.tap(options, fn);
		}
	}

	tapAsync(options, fn) {
		for (const hook of this.hooks) {
			hook.tapAsync(options, fn);
		}
	}

	tapPromise(options, fn) {
		for (const hook of this.hooks) {
			hook.tapPromise(options, fn);
		}
	}

	isUsed() {
		for (const hook of this.hooks) {
			if (hook.isUsed()) return true;
		}
		return false;
	}

	intercept(interceptor) {
		for (const hook of this.hooks) {
			hook.intercept(interceptor);
		}
	}

	withOptions(options) {
		return new MultiHook(
			this.hooks.map(h => h.withOptions(options)),
			this.name
		);
	}
}
复制代码

结语

Tapable 的源码就分析完了,对于 callAsyncpromise 方法拼接的过程,感兴趣的可以自己看一下。

整体来看,Tapable 的源码并不复杂,但是它却是支撑起整个 Webpack 插件的核心,里面的思想还是很值得学习的。

更多精彩,请关注微信公众号:闹闹前端

文章分类
前端
文章标签