上一篇文章《Webpack tapable 使用研究》研究了tapable的用法,了解用法有助于咱们理解源码。感兴趣能够看看。api
看源码,第一感受确定是充满疑惑的。数组
先从用法最简单的SyncHook来看吧。我想象的SyncHook大体是这样:promise
export default class SyncHook {
constructor() {
this.taps = [];
}
tap(name, fn) {
this.taps.push({
name,
fn,
});
}
call() {
this.taps.forEach(tap => tap.fn());
}
}
复制代码
有个tap方法,有个call方法,有个变量存储注册的插件,但是实际上不是:bash
const Hook = require("./Hook");
const HookCodeFactory = require("./HookCodeFactory");
class SyncHookCodeFactory extends HookCodeFactory {
...
}
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);
}
}
module.exports = SyncHook;
复制代码
没有tap也没有call,反而有tapAsync和tapPromise。还有个不知干啥的compile方法,里面还用了工厂。SyncHook继承自Hook。闭包
分析:tap和call方法确定是要有的,不在这里,那就在它的基类Hook里。这里使用到了继承和工厂模式,咱们能够经过源码学习它们的实践了。异步
咱们不急着看Hook.js,既然它用到继承,就是将公共的、可复用的逻辑抽象到父类中了。若是直接看父类,咱们可能不容易发现做者抽象的思路,为何要将这些点抽象到父类中。async
咱们先看看这些继承了Hook的子类,看看它们有那些公共的地方,再去看父类Hook.js。ide
// SyncBailHook.js
class SyncBailHookCodeFactory extends HookCodeFactory {
...
}
const factory = new SyncBailHookCodeFactory();
class SyncBailHook extends Hook {
tapAsync() {
throw new Error("tapAsync is not supported on a SyncBailHook");
}
tapPromise() {
throw new Error("tapPromise is not supported on a SyncBailHook");
}
compile(options) {
factory.setup(this, options);
return factory.create(options);
}
}
module.exports = SyncBailHook;
复制代码
SyncBailHook与SyncHook的区别就是换了个工厂给compile方法。其余没有什么不一样。SyncLoopHook.js、SyncWaterfallHook.js全都相似,只是使用的工厂不一样。函数
分析:仍是分析不出什么,同步的钩子看完了,接着在看异步钩子类。oop
const Hook = require("./Hook");
const HookCodeFactory = require("./HookCodeFactory");
class AsyncParallelHookCodeFactory extends HookCodeFactory {
...
}
const factory = new AsyncParallelHookCodeFactory();
class AsyncParallelHook extends Hook {
compile(options) {
factory.setup(this, options);
return factory.create(options);
}
}
Object.defineProperties(AsyncParallelHook.prototype, {
_call: { value: undefined, configurable: true, writable: true }
});
module.exports = AsyncParallelHook;
复制代码
连tapAsync和tapPromise的异常抛出都没有了,只剩compile方法了。下面还用Object.defineProperties给还AsyncParallelHook定义了一个_call方法。其余的异步钩子类,也跟AsyncParallelHook文件很相似,就是compile中使用的工厂不一样。将_call的value定义为null。
分析:这里用Object.defineProperties定义类方法是个疑惑点,为何不直接写在类中,而是用这种方式呢?
再就是说明各个Hook之间的主要区别,在于compile方法,compile方法里使用的不一样工厂类,也是主要的区别点。其余全部逻辑,都抽象到Hook.js里了。
咱们如今的疑惑,compile方法究竟是干啥的?
带着疑惑,咱们来看tapable有着最核心的逻辑的Hook.js文件,先省略一些部分,先看关键的api:
class Hook {
constructor(args) {
if (!Array.isArray(args)) args = [];
this._args = args;
this.taps = [];
this.interceptors = [];
this.call = this._call;
this.promise = this._promise;
this.callAsync = this._callAsync;
}
compile(options) {
throw new Error("Abstract: should be overriden");
}
tap(options, fn) {
...
}
tapAsync(options, fn) {
...
}
tapPromise(options, fn) {
...
}
intercept(interceptor) {
...
}
}
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
}
});
module.exports = Hook;
复制代码
先看构造函数,接收args的数组,做为插件的参数标识。taps变量存储插件,interceptors变量存储拦截器。
再看方法,compile方法在这,标识是个抽象方法,由子类重写,也符合咱们查看子类的预期。
tap、tapAsync、tapPromise、intercept在子类中都会被继承下来,可是在同步的钩子中,tapAsync、tapPromise被抛了异常了,不能用,也符合使用时的预期。
这里比较疑惑的是call、promise、callAsync这三个调用方法,为啥不像tap这样写在类里,而是写在构造函数的变量里,并且下面Object.defineProperties定义了三个_call、_promise、_callAsync三个私有方法,它们和call、promise、callAsync是什么关系?
咱们接着深刻的看。
既然调用方法call、promise、callAsync的实现比较复杂,咱们就先看tap、tapAsync、tapPromise这些注册方法,实现比较简单:
tap(options, fn) {
if (typeof options === "string") options = { name: options };
if (typeof options !== "object" || options === null)
throw new Error(
"Invalid arguments to tap(options: Object, fn: function)"
);
options = Object.assign({ type: "sync", fn: fn }, options);
if (typeof options.name !== "string" || options.name === "")
throw new Error("Missing name for tap");
options = this._runRegisterInterceptors(options);
this._insert(options);
}
tapAsync(options, fn) {
if (typeof options === "string") options = { name: options };
if (typeof options !== "object" || options === null)
throw new Error(
"Invalid arguments to tapAsync(options: Object, fn: function)"
);
options = Object.assign({ type: "async", fn: fn }, options);
if (typeof options.name !== "string" || options.name === "")
throw new Error("Missing name for tapAsync");
options = this._runRegisterInterceptors(options);
this._insert(options);
}
tapPromise(options, fn) {
if (typeof options === "string") options = { name: options };
if (typeof options !== "object" || options === null)
throw new Error(
"Invalid arguments to tapPromise(options: Object, fn: function)"
);
options = Object.assign({ type: "promise", fn: fn }, options);
if (typeof options.name !== "string" || options.name === "")
throw new Error("Missing name for tapPromise");
options = this._runRegisterInterceptors(options);
this._insert(options);
}
复制代码
它们三个的实现很是相似。核心功能是拼起一个options对象,options的内容以下:
options:{
name, // 插件名称
type: "sync" | "async" | "promise", // 插件注册的类型
fn, // 插件的回调函数,被call时的响应函数
stage, // 插件调用的顺序值
before,// 插件在哪一个插件以前调用
}
复制代码
拼好了options,就利用_insert方法将其放到taps变量里,以供后续调用。_insert方法内部就是实现了根据stage和before两个值,对options的插入到taps中的顺序作了调整并插入。
intercept方法将拦截器的相应回调放到interceptors里,以供对应的时机调用。
注册过程机会没什么区别,区别在于调用过程,最终影响插件的执行顺序和逻辑。
首先先解决为何_call方法要写成Object.defineProperties中定义,而不是类中定义,这样的好处是,方便咱们为_call方法赋值为另外一个函数,代码中将_call的value赋值成了createCompileDelegate方法的返回值,而若是将_call直接声明到类中,很差作到。再就是能够直接在子类(如AsyncParallelHook)中,再利用Object.defineProperties将_call的vale赋值为null。就能够获得一个没有_call方法的子类了。
再看一个私有方法:
_resetCompilation() {
this.call = this._call;
this.callAsync = this._callAsync;
this.promise = this._promise;
}
复制代码
此方法在_insert和intercept中调用,也就是在每次的注册新插件或注册新的拦截器,会触发一次私有调用方法到call等变量的一次赋值。
为何每次都要从新赋值呢?每次的_call方法不同了吗?我先给出答案,确实,每次赋值都是一个全新的new出来的_call方法。由于注册新插件或注册新的拦截器会造成一个新的_call方法,因此每次都要从新赋值一次。
那为何要每次生成一个新的_call方法呢?直接写死很差吗,不就是调用taps变量里的插件和拦截器吗?
缘由是由于咱们的插件彼此有着联系,因此咱们用了这么多类型的钩子来控制这些联系,每次注册了新的插件或拦截器,咱们就要从新排布插件和拦截器的调用顺序,因此每次都要生成新的_call方法。接下来咱们来看代码:
function createCompileDelegate(name, type) {
return function lazyCompileHook(...args) {
this[name] = this._createCall(type);
return this[name](...args);
};
}
复制代码
生成_call方法的是createCompileDelegate方法,这里用到了闭包,存储了name和type。而后返回了一个lazyCompileHook方法给_call变量。当_call方法被调用时,_createCall方法也当即被调用。
_createCall(type) {
return this.compile({
taps: this.taps,
interceptors: this.interceptors,
args: this._args,
type: type
});
}
复制代码
这里调用了compile方法,也就是说咱们的调用方法(call方法、callAsync方法、promise方法)和compile是息息相关的。看SyncHook中的compile
class SyncHookCodeFactory extends HookCodeFactory {
...
}
const factory = new SyncHookCodeFactory();
export default class SyncHook {
...
compile(options) {
factory.setup(this, options);
return factory.create(options);
}
}
复制代码
compile关联了HookCodeFactory,咱们来看HookCodeFactory的setup和create方法都干了什么:
setup(instance, options) {
instance._x = options.taps.map(t => t.fn);
}
复制代码
setup就是将插件的回调函数,都存在钩子实例的_x变量上。
create(options) {
this.init(options);
let fn;
switch (this.options.type) {
case "sync":
fn = new Function(
this.args(),
'"use strict";\n' +
this.header() +
this.content({
onError: err => `throw ${err};\n`,
onResult: result => `return ${result};\n`,
resultReturns: true,
onDone: () => "",
rethrowIfPossible: true
})
);
break;
...
}
复制代码
create方法咱们只关注跟Sync相关的,这里的变量fn就是最终在调用的时刻,生成了一个call方法的执行体。咱们来看一下这个生成的call方法什么样:
实验代码:
import { SyncHook } from 'tapable';
const hook = new SyncHook(['options']);
hook.tap('A', function (arg) {
console.log('A', arg);
})
hook.tap('B', function () {
console.log('b')
})
hook.call(6);
console.log(hook.call);
console.log(hook);
复制代码
打印结果以下:
能够看到咱们的call方法中的x就是setup方法中设置的咱们插件的回调函数啊,call方法生成的代码,就是根据咱们使用不一样的钩子,根据咱们设计的逻辑,调用这些回调。
在看一下hook对象下的call和callAsync有何不一样,callAsync没有被调用,因此它仍是lazyCompileHook函数,也验证了咱们的思考,call方法是在调用时,才被生成了上面那样的执行函数。
tapable的核心逻辑,就研究完毕了,感兴趣的小伙伴能够继续再看看。能够看到源码中对于面向对象继承的使用,工厂模式的使用,调用时才生成执行逻辑这种操做。都是值得咱们学习的。