webpack核心模块tapable源码解析 (2)

Hook类需要包含一些公共的代码,call这种不一样的部分由各个子类自己实现。所以Hook类就长这样:

const CALL_DELEGATE = function(...args) { this.call = this._createCall(); return this.call(...args); }; // Hook是SyncHook和SyncBailHook的基类 // 大体结构是一样的,不一样的地方是call // 不同子类的call是不一样的 // tapable的Hook基类提供了一个抽象接口compile来动态生成call函数 class Hook { constructor(args = []) { this._args = args; this.taps = []; // 基类的call初始化为CALL_DELEGATE // 为什么这里需要这样一个代理,而不是直接this.call = _createCall() // 等我们后面子类实现了再一起讲 this.call = CALL_DELEGATE; } // 一个抽象接口compile // 由子类实现,基类compile不能直接调用 compile(options) { throw new Error("Abstract: should be overridden"); } tap(name, fn) { this.taps.push(fn); } // _createCall调用子类实现的compile来生成call方法 _createCall() { return this.compile({ taps: this.taps, args: this._args, }); } }

官方对应的源码看这里:https://github.com/webpack/tapable/blob/master/lib/Hook.js

子类SyncHook实现

现在有了Hook基类,我们的SyncHook就需要继承这个基类重写,tapable在这里继承的时候并没有使用class extends,而是手动继承的:

const Hook = require('./Hook'); function SyncHook(args = []) { // 先手动继承Hook const hook = new Hook(args); hook.constructor = SyncHook; // 然后实现自己的compile函数 // compile的作用应该是创建一个call函数并返回 hook.compile = function(options) { // 这里call函数的实现跟前面实现是一样的 const { taps } = options; const call = function(...args) { const tapsLength = taps.length; for(let i = 0; i < tapsLength; i++) { const fn = this.taps[i]; fn(...args); } } return call; }; return hook; } SyncHook.prototype = null;

注意:我们在基类Hook构造函数中初始化this.call为CALL_DELEGATE这个函数,这是有原因的,最主要的原因是确保this的正确指向。思考一下假如我们不用CALL_DELEGATE,而是直接this.call = this._createCall()会发生什么?我们来分析下这个执行流程:

用户使用时,肯定是使用new SyncHook(),这时候会执行const hook = new Hook(args);

new Hook(args)会去执行Hook的构造函数,也就是会运行this.call = this._createCall()

这时候的this指向的是基类Hook的实例,this._createCall()会调用基类的this.compile()

由于基类的complie函数是一个抽象接口,直接调用会报错Abstract: should be overridden。

那我们采用this.call = CALL_DELEGATE是怎么解决这个问题的呢

采用this.call = CALL_DELEGATE后,基类Hook上的call就只是被赋值为一个代理函数而已,这个函数不会立马调用。

用户使用时,同样是new SyncHook(),里面会执行Hook的构造函数

Hook构造函数会给this.call赋值为CALL_DELEGATE,但是不会立即执行。

new SyncHook()继续执行,新建的实例上的方法hook.complie被覆写为正确方法。

当用户调用hook.call的时候才会真正执行this._createCall(),这里面会去调用this.complie()

这时候调用的complie已经是被正确覆写过的了,所以得到正确的结果。

子类SyncBailHook的实现

子类SyncBailHook的实现跟上面SyncHook的也是非常像,只是hook.compile实现不一样而已:

const Hook = require('./Hook'); function SyncBailHook(args = []) { // 基本结构跟SyncHook都是一样的 const hook = new Hook(args); hook.constructor = SyncBailHook; // 只是compile的实现是Bail版的 hook.compile = function(options) { const { taps } = options; const call = function(...args) { const tapsLength = taps.length; for(let i = 0; i < tapsLength; i++) { const fn = this.taps[i]; const res = fn(...args); if( res !== undefined) break; } } return call; }; return hook; } SyncBailHook.prototype = null; 抽象代码工厂

上面我们通过对SyncHook和SyncBailHook的抽象提炼出了一个基类Hook,减少了重复代码。基于这种结构子类需要实现的就是complie方法,但是如果我们将SyncHook和SyncBailHook的complie方法拿出来对比下:

SyncHook:

hook.compile = function(options) { const { taps } = options; const call = function(...args) { const tapsLength = taps.length; for(let i = 0; i < tapsLength; i++) { const fn = this.taps[i]; fn(...args); } } return call; };

SyncBailHook

hook.compile = function(options) { const { taps } = options; const call = function(...args) { const tapsLength = taps.length; for(let i = 0; i < tapsLength; i++) { const fn = this.taps[i]; const res = fn(...args); if( res !== undefined) return res; } } return call; };

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/wsfpxj.html