上一篇文章《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。markdown
分析:tap和call方法确定是要有的,不在这里,那就在它的基类Hook里。这里使用到了继承和工厂模式,咱们能够经过源码学习它们的实践了。闭包
咱们不急着看Hook.js,既然它用到继承,就是将公共的、可复用的逻辑抽象到父类中了。若是直接看父类,咱们可能不容易发现做者抽象的思路,为何要将这些点抽象到父类中。异步
咱们先看看这些继承了Hook的子类,看看它们有那些公共的地方,再去看父类Hook.js。async
// 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全都相似,只是使用的工厂不一样。ide
分析:仍是分析不出什么,同步的钩子看完了,接着在看异步钩子类。函数
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的核心逻辑,就研究完毕了,感兴趣的小伙伴能够继续再看看。能够看到源码中对于面向对象继承的使用,工厂模式的使用,调用时才生成执行逻辑这种操做。都是值得咱们学习的。