1 tapable 的介绍
webpack 内部的工作流程是基于插件机制的,webpack 内部负责编译的 Compiler 对象和负责创建 bundle 的 Compilation 对象都是基于 tapable 中的钩子函数,tapable 是 webpack 内部使用的一个流程管理工具。在阅读 webpack 前需要先了解 tapable。
tapable 控制钩子函数的订阅和发布。订阅发布模式旨在降低系统不同模块之间的耦合度,订阅发布模式定义了一种一对多的依赖关系,一指发布者,多指订阅者,依赖是订阅者对发布者的依赖,多个订阅者同时监听一个发布者。当发布者的状态发生变化时,会将该变化通知给订阅者,订阅者据此更新自己的状态。
下面是一个同步Hook的例子。
// 创建实例
const syncHook = new SyncHook(["name", "age"]);
// 注册事件
syncHook.tap("1", (name, age, sex) => {
console.log("1", name, age, sex);
return 1;
});
syncHook.tap("2", (name, age) => {
console.log("2", name, age);
return 2;
});
// 触发事件,让监听函数执行
syncHook.call("zs", 18, '男');
//1 zs 18 undefined
//2 zs 18
实例化钩子函数时接收一个数组,数组的长度是接收的参数个数,例如调用 call 方法传入的是三个参数,new SyncHook 时是两个参数,实际上 handler 只能接收到两个参数。
2 tapable 源码解读
源码地址(2.2.0):
https://github.com/webpack/tapable
2.1 总体介绍
tapable 的入口文件是 lib/index.js 文件,在 index.js 文件中只是将模块作了整合导出。tapable 中主要提供了同步与异步两种钩子,下图对每一种 Hook 的功能进行简单介绍。
同步与异步的钩子函数都是基于 Hook 生成一个对象,重写对象上的一些方法。本质是将我们传入的参数标准化之后,推入实例的 taps 数组中。taps 存放每次执行 tap 方法生成的 options 对象,是一个有序的列表。
事件订阅方法主要包括 tap、tapAsync、tapPromise,事件触发方法主要包括 call、callAsync、promise 方法。
2.2 源码方法解析
源码方法解析主要从方法订阅和发布两部分讲明。
在创建实例时,可以传两个参数,第一个是数组 args,第二个是 name 值。钩子函数创建一个 Hook 对象,将数据参数赋给 _args ,将 name 赋给 参数 name。构造函数指向钩子函数,同步钩子函数覆盖了 hook 的 tabAsync、tapPromise 方法,同步钩子函数不支持 tabAsync() 和 tapPromise() ,所有的钩子函数都必须重写 compile() 。
2.2.1 事件订阅
同步订阅使用 tap() 方法,异步订阅还可以使用tapAsync() 和 tapPromise() 方法。这三者的注册方式相同,都调用了 Hook 类中的 this._tap() 将内容配置插入到 taps 数组中,不同点是 _tap 方法中的类型值不同,tap 是 sync、tapAsync 是 async、tapPromise 是 promise。
_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);
}
三个注册事件的第一个参数是上面代码的 options 形参,该参数只能是一个非空字符串或者非空对象,如果该参数是字符串,改为一个 name 为传入字符串的对象,将传入的参数标准化之后重新保存在 options 参数中。调用 _runRegisterInterceptors() 方法,每次执行事件订阅方法时,传入的 options 都要经过 interceptor.register() 函数的逻辑,将拦截器的内容添加到 options 中。插入注册函数对象时,主要是通过 options 中的 before、stage 属性确定当前事件订阅的回调函数的位置(默认放在 taps 数组的最后一位),调整回调函数的执行顺序。
MultiHook 中事件订阅使用 for 循环去调用 Hook.js 中的订阅事件。
HookMap 中使用 for 函数去生成新的 Hook,然后再去调用 tap() 事件。
2.2.2 事件发布
触发事件主要使用 call、callAsync、promise 方法,都调用了 Hook 类中的 _createCall() 方法,只是 type 参数分别为 sync/async/promise,该方法直接返回了 this.compile 方法的执行结果。
_createCall(type) {
return this.compile({
taps: this.taps,
interceptors: this.interceptors,
args: this._args,
type: type
});
}
这里的 compile 方法是一个抽象方法,需要在具体的钩子函数里面去实现,根据不同的 type 生成各自的可执行函数。例如在 SyncHook.js 中的实现:
class SyncHookCodeFactory extends HookCodeFactory {
content({ onError, onDone, rethrowIfPossible }) {
return this.callTapsSeries({
onError: (i, err) => onError(err),
onDone,
rethrowIfPossible,
});
}
}
const factory = new SyncHookCodeFactory();
const COMPILE = function (options) {
factory.setup(this, options);
return factory.create(options);
};
compile() 使用了一个 factory 对象, factory 对象是子类的一个实例,子类都是基于 HookCodeFactory 工厂函数生成的,每个子类只定义了 content 方法。
setup() 方法过滤出 taps 数组中的注册函数 fn,保存到调用对象的 _x 变量上。
create() 方法根据传入钩子类型 type ,利用 Function 构造函数实例化出一个函数并返回。
create()方法的逻辑如下:
create(options) {
this.init(options); //初始化 options 和 _args
let fn;
switch (this.options.type) {
case "sync":
fn = new Function(this.args(),
'"use strict";\n' +
this.header() +
this.contentWithInterceptors()
);
break;
case "async":
fn = new Function(this.args(),
'"use strict";\n' +
this.header() +
this.contentWithInterceptors()
);
break;
case "promise":
fn = new Function(this.args(),code);
break;
}
this.deinit(); //重置参数,避免影响其他实例。
return fn; //返回编译生成的函数。
}
args() 方法生成 Function 构造函数的形参,将 _args 数组中的内容和 before、after 参数拼接在一起转为字符串。
header() 方法处理全局变量,将传入 compile 方法的参数赋值一份存为局部变量。
contentWithInterceptors() 方法中先检查是否有拦截器。如果有拦截器,在添加插件执行代码之前先加上拦截器。
所有子类中都实现了 content() 方法,调用子类中的 content() 方法处理差异部分。例如同步或部分异步串行钩子函数调用 callTapsSeries() 方法;异步并行的钩子函数中使用 callTapsParallel() 方法;循环类的钩子函数中使用 callTapsLooping() 方法。
调用 callTapsSeries() 方法的钩子函数中,从后向前遍历,把后面的回调函数字符串粘贴在当前回调函数字符串后面,要么执行 onResult() ,要么执行 onDone() 。
调用 callTapsParallel() 方法的钩子函数中, _counter 是注册函数的个数。
调用 callTapsLooping() 方法的钩子函数中,使用 do/while 循环,在 onResult 中改变 _loop 变量去控制是否循环,循环内部代码使用 callTapsSeries() 方法。
三个方法中都调用了 callTap() 方法,根据类型 type 的不同生成不同的函数内容。
3 Function执行代码例子
1. 返回结果result不影响事件的执行
//同步 SyncHook
(function anonymous(name, age) {
"use strict";
var _context;
var _x = this._x;
var _fn0 = _x[0];
_fn0(name, age);
var _fn1 = _x[1];
_fn1(name, age);
})();
// AsyncParallelHook
(function anonymous(name, age, _callback) {
"use strict";
var _context;
var _x = this._x;
do {
var _counter = 2;
var _done = function () {
_callback();
};
if (_counter <= 0) break;
var _fn0 = _x[0];
_fn0(name, age, function (_err0) {
if (_err0) {
if (_counter > 0) {
_callback(_err0);
_counter = 0;
}
} else {
if (--_counter === 0) _done();
}
});
if (_counter <= 0) break;
var _fn1 = _x[1];
_fn1(name, age, function (_err1) {
if (_err1) {
if (_counter > 0) {
_callback(_err1);
_counter = 0;
}
} else {
if (--_counter === 0) _done();
}
});
} while (false);
})()
//异步串行:AsyncSeriesHook
(function anonymous(name, age, _callback) {
"use strict";
var _context;
var _x = this._x;
function _next0() {
var _fn1 = _x[1];
_fn1(name, age, function (_err1) {
if (_err1) {
_callback(_err1);
} else {
_callback();
}
});
}
var _fn0 = _x[0];
_fn0(name, age, function (_err0) {
if (_err0) {
_callback(_err0);
} else {
_next0();
}
});
})()
2. 返回结果 result 不为 undefined,跳过剩下所有逻辑。
//同步 SyncBailHook
(function anonymous(name, age) {
"use strict";
var _context;
var _x = this._x;
var _fn0 = _x[0];
var _result0 = _fn0(name, age);
if (_result0 !== undefined) {
return _result0;
} else {
var _fn1 = _x[1];
var _result1 = _fn1(name, age);
if (_result1 !== undefined) {
return _result1;
} else {
}
}
}
})();
//AsyncParallelBailHook
(function anonymous(name, age, _callback) {
"use strict";
var _context;
var _x = this._x;
var _results = new Array(2);
var _checkDone = function () {
for (var i = 0; i < _results.length; i++) {
var item = _results[i];
if (item === undefined) return false;
if (item.result !== undefined) {
_callback(null, item.result);
return true;
}
if (item.error) {
_callback(item.error);
return true;
}
}
return false;
};
do {
var _counter = 2;
var _done = function () {
_callback();
};
if (_counter <= 0) break;
var _fn0 = _x[0];
_fn0(name, age, function (_err0, _result0) {
if (_err0) {
if (_counter > 0) {
if (
0 < _results.length &&
((_results.length = 1),
(_results[0] = { error: _err0 }),
_checkDone())
) {
_counter = 0;
} else {
if (--_counter === 0) _done();
}
}
} else {
if (_counter > 0) {
if (
0 < _results.length &&
(_result0 !== undefined && (_results.length = 1),
(_results[0] = { result: _result0 }),
_checkDone())
) {
_counter = 0;
} else {
if (--_counter === 0) _done();
}
}
}
});
if (_counter <= 0) break;
if (1 >= _results.length) {
if (--_counter === 0) _done();
} else {
var _fn1 = _x[1];
_fn1(name, age, function (_err1, _result1) {
if (_err1) {
if (_counter > 0) {
if (
1 < _results.length &&
((_results.length = 2),
(_results[1] = { error: _err1 }),
_checkDone())
) {
_counter = 0;
} else {
if (--_counter === 0) _done();
}
}
} else {
if (_counter > 0) {
if (
1 < _results.length &&
(_result1 !== undefined && (_results.length = 2),
(_results[1] = { result: _result1 }),
_checkDone())
) {
_counter = 0;
} else {
if (--_counter === 0) _done();
}
}
}
});
}
} while (false);
})()
// AsyncSeriesBailHook
(function anonymous(name, age, _callback) {
"use strict";
var _context;
var _x = this._x;
function _next0() {
var _fn1= _x[1];
_fn1(name, age, function (_err1, _result1) {
if (_err1) {
_callback(_err1);
} else {
if (_result1 !== undefined) {
_callback(null, _result1);
} else {
_callback();
}
}
});
}
var _fn0 = _x[0];
_fn0(name, age, function (_err0, _result0) {
if (_err0) {
_callback(_err0);
} else {
if (_result0 !== undefined) {
_callback(null, _result0);
} else {
_next0();
}
}
});
})()
//SyncWaterfallHook
(function anonymous(name, age) {
"use strict";
var _context;
var _x = this._x;
var _fn0 = _x[0];
var _result0 = _fn0(name, age);
if (_result0 !== undefined) {
name = _result0;
}
var _fn1 = _x[1];
var _result1 = _fn1(name, age);
if (_result1 !== undefined) {
name = _result1;
}
return name;
})()
// AsyncSeriesWaterfallHook
(function anonymous(name, _callback) {
"use strict";
var _context;
var _x = this._x;
function _next0() {
var _fn1 = _x[1];
_fn1(name, function (_err1, _result1) {
if (_err1) {
_callback(_err1);
} else {
if (_result1 !== undefined) {
name = _result1;
}
_callback(null, name);
}
});
}
var _fn0 = _x[0];
_fn0(name, function (_err0, _result0) {
if (_err0) {
_callback(_err0);
} else {
if (_result0 !== undefined) {
name = _result0;
}
_next0();
}
});
})()
4. 顺序循环执行订阅事件,直到所有函数结果 result === undefined。
//SyncLoopHook
(function anonymous(name, age) {
"use strict";
var _context;
var _x = this._x;
var _loop;
do {
_loop = false;
var _fn0 = _x[0];
var _result0 = _fn0(name, age);
if (_result0 !== undefined) {
_loop = true;
} else {
var _fn1 = _x[1];
var _result1 = _fn1(name, age);
if (_result1 !== undefined) {
_loop = true;
} else {
if (!_loop) {
}
}
}
}
} while (_loop);
})()
//AsyncSeriesLoopHook:
(function anonymous(name, age, _callback) {
"use strict";
var _context;
var _x = this._x;
var _looper = function () {
var _loopAsync = false;
var _loop;
do {
_loop = false;
function _next0() {
var _fn1 = _x[1];
_fn1(name, age, function (_err1, _result1) {
if (_err1) {
_callback(_err1);
} else {
if (_result1 !== undefined) {
_loop = true;
if (_loopAsync) _looper();
} else {
_callback();
}
}
});
}
var _fn0 = _x[0];
_fn0(name, age, function (_err0, _result0) {
if (_err0) {
_callback(_err0);
} else {
if (_result0 !== undefined) {
_loop = true;
if (_loopAsync) _looper();
} else {
_next0();
}
}
});
} while (_loop);
_loopAsync = true;
};
_looper();
})();
4 总结
在tapable的触发事件中,使用了惰性函数,惰性函数是解决每次都要进行判断的问题,解决原理是重写函数。当第一次运行触发函数,例如 call 时,会执行到 CALL_DELEGATE 函数中,重新赋值 call 函数,下次执行call 函数时不用再次运行生成 call 的过程。
const CALL_DELEGATE = function(...args) {
this.call = this._createCall("sync");
return this.call(...args);
};