上一篇文章我写了tapable
的基本用法,咱们知道他是一个加强版版的发布订阅模式
,本文想来学习下他的源码。tapable
的源码我读了一下,发现他的抽象程度比较高,直接扎进去反而会让人云里雾里的,因此本文会从最简单的SyncHook
和发布订阅模式
入手,再一步一步抽象,慢慢变成他源码的样子。javascript
本文可运行示例代码已经上传GitHub,你们拿下来一边玩一边看文章效果更佳:https://github.com/dennis-jiang/Front-End-Knowledges/tree/master/Examples/Engineering/tapable-source-code。前端
SyncHook
的基本实现上一篇文章已经讲过SyncHook
的用法了,我这里就再也不展开了,他使用的例子就是这样子:java
const { SyncHook } = require("tapable"); // 实例化一个加速的hook const accelerate = new SyncHook(["newSpeed"]); // 注册第一个回调,加速时记录下当前速度 accelerate.tap("LoggerPlugin", (newSpeed) => console.log("LoggerPlugin", `加速到${newSpeed}`) ); // 再注册一个回调,用来检测是否超速 accelerate.tap("OverspeedPlugin", (newSpeed) => { if (newSpeed > 120) { console.log("OverspeedPlugin", "您已超速!!"); } }); // 触发一下加速事件,看看效果吧 accelerate.call(500);
其实这种用法就是一个最基本的发布订阅模式
,我以前讲发布订阅模式的文章讲过,咱们能够仿照那个很快实现一个SyncHook
:webpack
class SyncHook { constructor(args = []) { this._args = args; // 接收的参数存下来 this.taps = []; // 一个存回调的数组 } // tap实例方法用来注册回调 tap(name, fn) { // 逻辑很简单,直接保存下传入的回调参数就行 this.taps.push(fn); } // call实例方法用来触发事件,执行全部回调 call(...args) { // 逻辑也很简单,将注册的回调一个一个拿出来执行就行 const tapsLength = this.taps.length; for(let i = 0; i < tapsLength; i++) { const fn = this.taps[i]; fn(...args); } } }
这段代码很是简单,是一个最基础的发布订阅模式
,使用方法跟上面是同样的,将SyncHook
从tapable
导出改成使用咱们本身的:git
// const { SyncHook } = require("tapable"); const { SyncHook } = require("./SyncHook");
运行效果是同样的:github
注意: 咱们构造函数里面传入的args
并无用上,tapable
主要是用它来动态生成call
的函数体的,在后面讲代码工厂的时候会看到。web
SyncBailHook
的基本实现再来一个SyncBailHook
的基本实现吧,SyncBailHook
的做用是当前一个回调返回不为undefined
的值的时候,阻止后面的回调执行。基本使用是这样的:segmentfault
const { SyncBailHook } = require("tapable"); // 使用的是SyncBailHook const accelerate = new SyncBailHook(["newSpeed"]); accelerate.tap("LoggerPlugin", (newSpeed) => console.log("LoggerPlugin", `加速到${newSpeed}`) ); // 再注册一个回调,用来检测是否超速 // 若是超速就返回一个错误 accelerate.tap("OverspeedPlugin", (newSpeed) => { if (newSpeed > 120) { console.log("OverspeedPlugin", "您已超速!!"); return new Error('您已超速!!'); } }); // 因为上一个回调返回了一个不为undefined的值 // 这个回调不会再运行了 accelerate.tap("DamagePlugin", (newSpeed) => { if (newSpeed > 300) { console.log("DamagePlugin", "速度实在太快,车子快散架了。。。"); } }); accelerate.call(500);
他的实现跟上面的SyncHook
也很是像,只是call
在执行的时候不同而已,SyncBailHook
须要检测每一个回调的返回值,若是不为undefined
就终止执行后面的回调,因此代码实现以下:数组
class SyncBailHook { constructor(args = []) { this._args = args; this.taps = []; } tap(name, fn) { this.taps.push(fn); } // 其余代码跟SyncHook是同样的,就是call的实现不同 // 须要检测每一个返回值,若是不为undefined就终止执行 call(...args) { const tapsLength = this.taps.length; for(let i = 0; i < tapsLength; i++) { const fn = this.taps[i]; const res = fn(...args); if( res !== undefined) return res; } } }
而后改下SyncBailHook
从咱们本身的引入就行:架构
// const { SyncBailHook } = require("tapable"); const { SyncBailHook } = require("./SyncBailHook");
运行效果是同样的:
如今咱们只实现了SyncHook
和SyncBailHook
两个Hook
而已,上一篇讲用法的文章里面总共有9个Hook
,若是每一个Hook
都像前面这样实现也是能够的。可是咱们再仔细看下SyncHook
和SyncBailHook
两个类的代码,发现他们除了call
的实现不同,其余代码如出一辙,因此做为一个有追求的工程师,咱们能够把这部分重复的代码提出来做为一个基类:Hook
类。
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
如今有了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
的实现跟上面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; };
咱们发现这两个complie
也很是像,有大量重复代码,因此tapable
为了解决这些重复代码,又进行了一次抽象,也就是代码工厂HookCodeFactory
。HookCodeFactory
的做用就是用来生成complie
返回的call
函数体,而HookCodeFactory
在实现时也采用了Hook
相似的思路,也是先实现了一个基类HookCodeFactory
,而后不一样的Hook
再继承这个类来实现本身的代码工厂,好比SyncHookCodeFactory
。
在继续深刻代码工厂前,咱们先来回顾下JS里面建立函数的方法。通常咱们会有这几种方法:
函数申明
function add(a, b) { return a + b; }
函数表达式
const add = function(a, b) { return a + b; }
可是除了这两种方法外,还有种不经常使用的方法:使用Function构造函数。好比上面这个函数使用构造函数建立就是这样的:
const add = new Function('a', 'b', 'return a + b;');
上面的调用形式里,最后一个参数是函数的函数体,前面的参数都是函数的形参,最终生成的函数跟用函数表达式的效果是同样的,能够这样调用:
add(1, 2); // 结果是3
注意:上面的a
和b
形参放在一块儿用逗号隔开也是能够的:
const add = new Function('a, b', 'return a + b;'); // 这样跟上面的效果是同样的
固然函数并非必定要有参数,没有参数的函数也能够这样建立:
const sayHi = new Function('alert("Hello")'); sayHi(); // Hello
这样建立函数和前面的函数申明和函数表达式有什么区别呢?使用Function构造函数来建立函数最大的一个特征就是,函数体是一个字符串,也就是说咱们能够动态生成这个字符串,从而动态生成函数体。由于SyncHook
和SyncBailHook
的call
函数很像,咱们能够像拼一个字符串那样拼出他们的函数体,为了更简单的拼凑,tapable
最终生成的call
函数里面并无循环,而是在拼函数体的时候就将循环展开了,好比SyncHook
拼出来的call
函数的函数体就是这样的:
"use strict"; var _x = this._x; var _fn0 = _x[0]; _fn0(newSpeed); var _fn1 = _x[1]; _fn1(newSpeed);
上面代码的_x
其实就是保存回调的数组taps
,这里重命名为_x
,我想是为了节省代码大小吧。这段代码能够看到,_x
,也就是taps
里面的内容已经被展开了,是一个一个取出来执行的。
而SyncBailHook
最终生成的call
函数体是这样的:
"use strict"; var _x = this._x; var _fn0 = _x[0]; var _result0 = _fn0(newSpeed); if (_result0 !== undefined) { return _result0; ; } else { var _fn1 = _x[1]; var _result1 = _fn1(newSpeed); if (_result1 !== undefined) { return _result1; ; } else { } }
这段生成的代码主体逻辑其实跟SyncHook
是同样的,都是将_x
展开执行了,他们的区别是SyncBailHook
会对每次执行的结果进行检测,若是结果不是undefined
就直接return
了,后面的回调函数就没有机会执行了。
基于这个目的,咱们的代码工厂基类应该能够生成最基本的call
函数体。咱们来写个最基本的HookCodeFactory
吧,目前他只能生成SyncHook
的call
函数体:
class HookCodeFactory { constructor() { // 构造函数定义两个变量 this.options = undefined; this._args = undefined; } // init函数初始化变量 init(options) { this.options = options; this._args = options.args.slice(); } // deinit重置变量 deinit() { this.options = undefined; this._args = undefined; } // args用来将传入的数组args转换为New Function接收的逗号分隔的形式 // ['arg1', 'args'] ---> 'arg1, arg2' args() { return this._args.join(", "); } // setup其实就是给生成代码的_x赋值 setup(instance, options) { instance._x = options.taps.map(t => t); } // create建立最终的call函数 create(options) { this.init(options); let fn; // 直接将taps展开为平铺的函数调用 const { taps } = options; let code = ''; for (let i = 0; i < taps.length; i++) { code += ` var _fn${i} = _x[${i}]; _fn${i}(${this.args()}); ` } // 将展开的循环和头部链接起来 const allCodes = ` "use strict"; var _x = this._x; ` + code; // 用传进来的参数和生成的函数体建立一个函数出来 fn = new Function(this.args(), allCodes); this.deinit(); // 重置变量 return fn; // 返回生成的函数 } }
上面代码最核心的其实就是create
函数,这个函数会动态建立一个call
函数并返回,因此SyncHook
能够直接使用这个factory
建立代码了:
// SyncHook.js const Hook = require('./Hook'); const HookCodeFactory = require("./HookCodeFactory"); const factory = new HookCodeFactory(); // COMPILE函数会去调用factory来生成call函数 const COMPILE = function(options) { factory.setup(this, options); return factory.create(options); }; function SyncHook(args = []) { const hook = new Hook(args); hook.constructor = SyncHook; // 使用HookCodeFactory来建立最终的call函数 hook.compile = COMPILE; return hook; } SyncHook.prototype = null;
SyncBailHook
如今咱们的HookCodeFactory
只能生成最简单的SyncHook
代码,咱们须要对他进行一些改进,让他可以也生成SyncBailHook
的call
函数体。你能够拉回前面再仔细观察下这两个最终生成代码的区别:
SyncBailHook
须要对每次执行的result
进行处理,若是不为undefined
就返回SyncBailHook
生成的代码实际上是if...else
嵌套的,咱们生成的时候能够考虑使用一个递归函数为了让SyncHook
和SyncBailHook
的子类代码工厂可以传入差别化的result
处理,咱们先将HookCodeFactory
基类的create
拆成两部分,将代码拼装的逻辑单独拆成一个函数:
class HookCodeFactory { // ... // 省略其余同样的代码 // ... // create建立最终的call函数 create(options) { this.init(options); let fn; // 拼装代码头部 const header = ` "use strict"; var _x = this._x; `; // 用传进来的参数和函数体建立一个函数出来 fn = new Function(this.args(), header + this.content()); // 注意这里的content函数并无在基类HookCodeFactory实现,而是子类实现的 this.deinit(); return fn; } // 拼装函数体 // callTapsSeries也没在基类调用,而是子类调用的 callTapsSeries() { const { taps } = this.options; let code = ''; for (let i = 0; i < taps.length; i++) { code += ` var _fn${i} = _x[${i}]; _fn${i}(${this.args()}); ` } return code; } }
上面代码里面要特别注意create
函数里面生成函数体的时候调用的是this.content
,可是this.content
并没与在基类实现,这要求子类在使用HookCodeFactory
的时候都须要继承他并实现本身的content
函数,因此这里的content
函数也是一个抽象接口。那SyncHook
的代码就应该改为这样:
// SyncHook.js // ... 省略其余同样的代码 ... // SyncHookCodeFactory继承HookCodeFactory并实现content函数 class SyncHookCodeFactory extends HookCodeFactory { content() { return this.callTapsSeries(); // 这里的callTapsSeries是基类的 } } // 使用SyncHookCodeFactory来建立factory const factory = new SyncHookCodeFactory(); const COMPILE = function (options) { factory.setup(this, options); return factory.create(options); };
注意这里:子类实现的content
其实又调用了基类的callTapsSeries
来生成最终的函数体。因此这里这几个函数的调用关系实际上是这样的:
那这样设计的目的是什么呢?为了让子类content
可以传递参数给基类callTapsSeries
,从而生成不同的函数体。咱们立刻就能在SyncBailHook
的代码工厂上看到了。
为了可以生成SyncBailHook
的函数体,咱们须要让callTapsSeries
支持一个onResult
参数,就是这样:
class HookCodeFactory { // ... 省略其余相同的代码 ... // 拼装函数体,须要支持options.onResult参数 callTapsSeries(options) { const { taps } = this.options; let code = ''; let i = 0; const onResult = options && options.onResult; // 写一个next函数来开启有onResult回调的函数体生成 // next和onResult相互递归调用来生成最终的函数体 const next = () => { if(i >= taps.length) return ''; const result = `_result${i}`; const code = ` var _fn${i} = _x[${i}]; var ${result} = _fn${i}(${this.args()}); ${onResult(i++, result, next)} `; return code; } // 支持onResult参数 if(onResult) { code = next(); } else { // 没有onResult参数的时候,即SyncHook跟以前保持同样 for(; i< taps.length; i++) { code += ` var _fn${i} = _x[${i}]; _fn${i}(${this.args()}); ` } } return code; } }
而后咱们的SyncBailHook
的代码工厂在继承工厂基类的时候须要传一个onResult
参数,就是这样:
const Hook = require('./Hook'); const HookCodeFactory = require("./HookCodeFactory"); // SyncBailHookCodeFactory继承HookCodeFactory并实现content函数 // content里面传入定制的onResult函数,onResult回去调用next递归生成嵌套的if...else... class SyncBailHookCodeFactory extends HookCodeFactory { content() { return this.callTapsSeries({ onResult: (i, result, next) => `if(${result} !== undefined) {\nreturn ${result};\n} else {\n${next()}}\n`, }); } } // 使用SyncHookCodeFactory来建立factory const factory = new SyncBailHookCodeFactory(); const COMPILE = function (options) { factory.setup(this, options); return factory.create(options); }; function SyncBailHook(args = []) { // 基本结构跟SyncHook都是同样的 const hook = new Hook(args); hook.constructor = SyncBailHook; // 使用HookCodeFactory来建立最终的call函数 hook.compile = COMPILE; return hook; }
如今运行下代码,效果跟以前同样的,大功告成~
到这里,tapable
的源码架构和基本实现咱们已经弄清楚了,可是本文只用了SyncHook
和SyncBailHook
作例子,其余的,好比AsyncParallelHook
并无展开讲。由于AsyncParallelHook
之类的其余Hook
的实现思路跟本文是同样的,好比咱们能够先实现一个独立的AsyncParallelHook
类:
class AsyncParallelHook { constructor(args = []) { this._args = args; this.taps = []; } tapAsync(name, task) { this.taps.push(task); } callAsync(...args) { // 先取出最后传入的回调函数 let finalCallback = args.pop(); // 定义一个 i 变量和 done 函数,每次执行检测 i 值和队列长度,决定是否执行 callAsync 的最终回调函数 let i = 0; let done = () => { if (++i === this.taps.length) { finalCallback(); } }; // 依次执行事件处理函数 this.taps.forEach(task => task(...args, done)); } }
而后对他的callAsync
函数进行抽象,将其抽象到代码工厂类里面,使用字符串拼接的方式动态构造出来就好了,总体思路跟前面是同样的。具体实现过程能够参考tapable
源码:
本文可运行示例代码已经上传GitHub,你们拿下来一边玩一边看文章效果更佳:https://github.com/dennis-jiang/Front-End-Knowledges/tree/master/Examples/Engineering/tapable-source-code。
下面再对本文的思路进行一个总结:
tapable
的各类Hook
其实都是基于发布订阅模式。Hook
本身独立实现其实也没有问题,可是由于都是发布订阅模式,会有大量重复代码,因此tapable
进行了几回抽象。Hook
基类,这个基类实现了初始化和事件注册等公共部分,至于每一个Hook
的call
都不同,须要本身实现。Hook
在实现本身的call
的时候,发现代码也有不少类似之处,因此提取了一个代码工厂,用来动态生成call
的函数体。tapable
的代码并不难,可是由于有两次抽象,整个代码架构显得不那么好读,通过本文的梳理后,应该会好不少了。文章的最后,感谢你花费宝贵的时间阅读本文,若是本文给了你一点点帮助或者启发,请不要吝啬你的赞和GitHub小星星,你的支持是做者持续创做的动力。
欢迎关注个人公众号进击的大前端第一时间获取高质量原创~
“前端进阶知识”系列文章源码地址: https://github.com/dennis-jiang/Front-End-Knowledges
tapable
用法介绍:http://www.javashuo.com/article/p-rgjasasi-vg.html
tapable
源码地址:https://github.com/webpack/tapable