Webpack之Tapable源码解读

1,039 阅读5分钟

前言

Webpack官方文档在讲述Plugin API有如下描述:

tapable 这个小型 library 是 webpack 的一个核心工具,但也可用于其他地方,以提供类似的插件接口。webpack 中许多对象扩展自 Tapable 类。这个类暴露 tap, tapAsync 和 tapPromise 方法,可以使用这些方法,注入自定义的构建步骤,这些步骤将在整个编译过程中不同时机触发。

正文

Tapable对外暴露的Hook可分为同步和异步两种类型, 这两种类型在执行时又可以分为并行和串行两种方式. 具体如下:

其中各个Hook的介绍如下:

序号钩子名称执行方式使用要点
1SyncHook同步串行不关心监听函数的返回值
2SyncBailHook同步串行只要监听函数中有一个函数的返回值为非 undefined,则跳过剩下所有的逻辑
3SyncWaterfallHook同步串行上一个监听函数的返回值可以传给下一个监听函数
4SyncLoopHook同步循环当监听函数被触发的时候,如果该监听函数返回true时则这个监听函数会反复执行,如果返回 undefined 则表示退出循环
5AsyncParallelHook异步并发不关心监听函数的返回值
6AsyncParallelBailHook异步并发只要监听函数的返回值不为undefined,就会忽略后面的监听函数执行,直接跳跃到callAsync等触发函数绑定的回调函数,然后执行这个被绑定的回调函数
7AsyncSeriesHook异步串行不关系callback()的参数
8AsyncSeriesBailHook异步串行callback()的参数不为undefined,就会直接执行callAsync等触发函数绑定的回调函数
9AsyncSeriesWaterfallHook异步串行上一个监听函数的中的callback(err, data)的第二个参数,可以作为下一个监听函数的参数

例子

class Car {
	constructor() { this.hooks = { accelerate: new SyncHook(["newSpeed"]) }; } // 声明钩子

	// 钩子调用
	setSpeed(newSpeed) { this.hooks.accelerate.call(newSpeed); }
}

// 声明实例
const myCar = new Car();

// 注册事件1
myCar.hooks.accelerate.tap("LoggerPlugin", newSpeed => console.log(`[LoggerPlugin]Accelrating to ${newSpeed}`));

// 注册事件2
myCar.hooks.accelerate.tap(
	{ name: "LoggerPlugin2", before: "LoggerPlugin" },
	newSpeed => console.log(`[LoggerPlugin1]Accelrating to ${newSpeed}`)
);

// 注册事件3
myCar.hooks.accelerate.tap({
	name: "LoggerPlugin3",
	fn: newSpeed => console.log(`[LoggerPlugin3]Accelrating to ${newSpeed}`)
});

myCar.hooks.accelerate.intercept({
	call: newSpeed => console.log(`The newSpeed is: ${newSpeed}`),
	tap: tapInfo => console.log(`${tapInfo.name} tap was trigged!`),
	register: tapInfo => {
		console.log(`${tapInfo.name} is doing its jos`, JSON.stringify(tapInfo));
		return tapInfo;
	}
});

myCar.setSpeed(60);

执行结果如下:

LoggerPlugin2 is doing its jos {"type":"sync","name":"LoggerPlugin2","before":"LoggerPlugin"}
LoggerPlugin is doing its jos {"type":"sync","name":"LoggerPlugin"}
LoggerPlugin3 is doing its jos {"type":"sync","name":"LoggerPlugin3"}
The newSpeed is: 60
LoggerPlugin2 tap was trigged!
[LoggerPlugin1]Accelrating to 60
LoggerPlugin tap was trigged!
[LoggerPlugin]Accelrating to 60
LoggerPlugin3 tap was trigged!
[LoggerPlugin3]Accelrating to 60

具体来说, 使用Tapable Hook使用大致分为三步.

  1. 声明实例 var sync = new SyncHook(['a', 'b'])
  2. 注册若干事件 sync.tap('name', fn)
  3. 通过sync.call(a, b)调用第2步注册的事件(调用时call时, 才会调用_createCall生成call的执行逻辑)

生成的call方法见文章最后部分.

源码解读

核心代码分布在Hook.jsHookCodeFactory.js两个文件中. 其中前者主要实现了注册事件的逻辑, 目的是提供一个可供其他Hook继承的基类, 而后者提供生成call, callAsynccallPromise方法方法体的逻辑用于生成执行注册方法的逻辑create.

我们使用上述例子中使用的SyncHook来解读一下源码, 其他的Hook实现大同小异.

SyncHook

核心代码如下

class SyncHookCodeFactory extends HookCodeFactory {
	// 在HookCodeFactory contentWithInterceptors中使用, 生成执行逻辑代码
	content({ onError, onDone, rethrowIfPossible }) {
		return this.callTapsSeries({
			onError: (i, err) => onError(err),
			onDone,
			rethrowIfPossible
		});
	}
}

const factory = new SyncHookCodeFactory();

// 生成执行逻辑的方法体
const COMPILE = function(options) {
	// setup, create方法见HookCodeFactory
	factory.setup(this, options);
	return factory.create(options);
};

function SyncHook(args = [], name = undefined) {
	// 这里使用Hook初始化一个基础钩子
	const hook = new Hook(args, name);
	hook.constructor = SyncHook;
	hook.tapAsync = TAP_ASYNC;
	hook.tapPromise = TAP_PROMISE;
	hook.compile = COMPILE;
	return hook;
}

几个关键点:

  1. new Hook() 用于初始化一个基础Hook实例
  2. COMPILE封装了生成call执行逻辑的代码, 在调用call时会调用(见下方CALL_DELEGATE)
  3. content方法用于在执行COMPILE方法时的一些定制逻辑.

Hook.js

核心代码:

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

class Hook {
	constructor(args = [], name = undefined) {
        // 省略代码...
        this.taps = [];
        this.call = CALL_DELEGATE;
	}
	// 这里并没有实现compile逻辑, 而是在各个Hook类中对compile进行赋值, 进而在HookCodeFactory中生成执行代码
    // 参考上面AsyncHook中的COMPILE
	compile(options) {
		throw new Error("Abstract: should be overridden");
	}

	_createCall(type) {
		return this.compile({ //省略代码...
        });
	}

	_tap(type, options, fn) {
		// 省略代码...
		options = Object.assign({ type, fn }, options);
        // 传入配置参数, _runRegisterInterceptors拦截器中的register可以对options定制化修改并返回新的配置
		options = this._runRegisterInterceptors(options);
		this._insert(options);
	}
    
	// 注册事件
	tap(options, fn) {
		this._tap("sync", options, fn);
	}

	// 向taps事件队列中添加新的声明方法并调整注册方法的执行顺序, 
	_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;
	}
}

关键点:

  1. tap方法用于注册普通事件(tapAsync, tapPromise用于注册异步事件)
  2. 核心方法_tap会调用_insert向注册的事件队列中添加事件, 并调整事件执行顺序(如果注册事件时传入了stage, before参数的话).

题外话: _insert中调整事件执行顺序比较绕, 可以看看笔者阅读时添加的注释. stage, before使用方法参考Hook.js测试代码

HookCodeFactory.js

核心是create方法, 具体逻辑就是通过new Function方式进行字符串拼接, 生成传参及call方法的方法体. 可以学习代码下载到本地, 自己动手执行下.

myCar.hooks.accelerate.call

newSpeed => {
	var _context;
	var _x = this._x;
	var _taps = this.taps;
	var _interceptors = this.interceptors;
	_interceptors[0].call(newSpeed);
	var _tap0 = _taps[0];
	_interceptors[0].tap(_tap0);
	var _fn0 = _x[0];
	_fn0(newSpeed);
	var _tap1 = _taps[1];
	_interceptors[0].tap(_tap1);
	var _fn1 = _x[1];
	_fn1(newSpeed);
	var _tap2 = _taps[2];
	_interceptors[0].tap(_tap2);
	var _fn2 = _x[2];
	_fn2(newSpeed);
};

想说的话

核心代码虽然将注册及实现做了分离, 实现了各个Hook的解耦, 但是增加了理解难度, 另外在create方法的具体实现上感觉还有优化空间, 当前的拼接方式无疑增加了记忆成本, 大佬除外...

不妥之处, 还望指出!!