You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
由于在 webpack 打包构建的过程中,会有上千(数量其实是取决于自身业务复杂度)个插件钩子执行,同时同类型的钩子在执行时,函数参数固定,函数体相同,因此 tapable 针对这些业务场景进行了相应的优化。这其中最重要的是运用了单态性及多态性概念,内联缓存的原理,也可以看这个issue。为了达到这个目标,tapable 采用 new Function 动态生成函数执行体的方式,主要逻辑在源码的 HookCodeFactory.js文件中。
callFn 及 callFn2 的目的都是为了实现将一组方法以相同的参数调用,依次执行。很显然,方法一效率明显更高,并且容易扩展,能支持传入数量不固定的一组方法。但是,如果根据单态性以及内联缓存的说法,很明显方法二的执行效率更高,同时也存在一个问题,即只支持传入a,b,c三个方法,参数形态也固定,这种方式显然没有方法一灵活,那能不能同时兼顾效率以及灵活性呢?答案是可以的。我们可以借助 new Function 动态生成函数体的方式。
testhook.tap("plugin1");testhook.tap("plugin2");testhook.tap("plugin3");testhook.call(compilation,"my test 1");// 第一次调用 call 时,会调用CALL_DELEGATE动态生成函数执行体并缓存起来testhook.call(compilation,"my test 2");// 不会重新生成函数执行体,使用第一次的testhook.call(compilation,"my test 3");// 不会重新生成函数执行体,使用第一次的
避免下面的调用方式:
testhook.tap("plugin1");testhook.call(compilation,"my test 1");// 第一次调用 call 时,会调用CALL_DELEGATE动态生成函数执行体并缓存起来testhook.tap("plugin2");testhook.call(compilation,"my test 2");// 重新调用CALL_DELEGATE生成函数执行体testhook.tap("plugin3");testhook.call(compilation,"my test 3");// 重新调用CALL_DELEGATE生成函数执行体
完整的手写源码仓库
目录
tapable hook
的源码,每个hook
都用自己的思路实现一遍,并且和官方的hook
执行时间做个对比。tapable的设计理念:单态、多态及内联缓存
由于在
webpack
打包构建的过程中,会有上千(数量其实是取决于自身业务复杂度)个插件钩子执行,同时同类型的钩子在执行时,函数参数固定,函数体相同,因此tapable
针对这些业务场景进行了相应的优化。这其中最重要的是运用了单态性及多态性概念,内联缓存的原理,也可以看这个issue。为了达到这个目标,tapable
采用new Function
动态生成函数执行体的方式,主要逻辑在源码的 HookCodeFactory.js文件中。如何理解 tapable 的设计理念
思考下面两种实现方法,哪一种执行效率高,哪一种实现方式简洁?
callFn
及callFn2
的目的都是为了实现将一组方法以相同的参数调用,依次执行。很显然,方法一效率明显更高,并且容易扩展,能支持传入数量不固定的一组方法。但是,如果根据单态性以及内联缓存的说法,很明显方法二的执行效率更高,同时也存在一个问题,即只支持传入a,b,c三个方法,参数形态也固定,这种方式显然没有方法一灵活,那能不能同时兼顾效率以及灵活性呢?答案是可以的。我们可以借助new Function
动态生成函数体的方式。当我们在浏览器控制台执行上述代码时:
拼接后的完整函数执行体:
可以看到,通过这种动态生成函数执行体的方式,我们能够同时兼顾性能及灵活性。我们可以通过
tap
方法添加任意数量的任务,同时通过在初始化构造函数时new HookCodeFactory(['x', 'y', ..., 'n'])
传入任意参数。实际上,这正是官方
tapable
的HookCodeFactory.js的简化版本。这是tapable
的精华所在。tapable源码解读
tapable
最主要的源码在Hook.js
以及HookCodeFactory.js
中。Hook.js
主要是提供了tap
、tapAsync
、tapPromise
等方法,每个Hook
都在构造函数内部调用const hook = new Hook()
初始化hook
实例。HookCodeFactory.js
主要是根据new Function
动态生成函数执行体。demo
以
SyncHook.js
为例,SyncHook
钩子使用如下:我们用这个demo做为用例,一步步debug。
SyncHook.js
源码主要逻辑如下:
Hook.js
源码主要逻辑如下:
思考Hook.js源码中的几个问题
this.compile
、this.tap
、this.tapAsync
以及this.tapPromise
等方法当我们每次调用
testhook.tap
方法注册插件时,流程如下:方法往
this.taps
数组中添加一个插件。this.__insert
方法逻辑比较简单,但这里有一个细节需要注意一下,为什么每次注册插件时,都需要调用this._resetCompilation()重置this.call等方法? 我们稍后再看下这个问题。先继续debug。当我们 第一次(注意是第一次) 调用
testhook.call
时,实际上调用的是CALL_DELEGATE
方法CALL_DELEGATE
调用this._createCall
函数根据注册的this.taps
动态生成函数执行体。并且this.call
被设置成this._createCall
的返回值缓存起来,如果this.taps
改变了,则需要重新生成。此时如果我们第二次调用
testhook.call
时,就不需要再重新动态生成一遍函数执行体。这也是tapable的优化技巧之一。这也回答了 问题一:为什么需要 CALL_DELEGATE。如果我们调用了n次
testhook.call
,然后又调用testhook.tap
注册插件,此时this.call
已经不能重用了,需要再根据CALL_DELEGATE
重新生成一次函数执行体,这也回答了问题二:为什么每次调用 testhook.tap() 注册插件时,都需要重置this.call等方法。可想而知重新生成的过程是很耗时的。因此我们在使用tapable
时,最好一次性注册完所有插件,再调用call
避免下面的调用方式:
现在让我们看看第三个问题,调用
this.compile
方法时,实际上会调用HookCodeFacotry.js
中的setup
方法:对于问题四,实际上这和 V8 引擎的
Hidden Class
有关,通过在构造函数中绑定这些方法,类中的属性形态固定,这样在查找这些方法时就能利用 V8 引擎中Hidden Class
属性查找机制,提高性能。HookCodeFactory.js
主要逻辑:
手写 tapable 每个 Hook
手写 tapable中所有的 hook,并比较我们自己实现的 hook 和官方的执行时间
这里面每个文件都会实现一遍 官方的 hook,并比较执行时间,以 SyncHook 为例,批量注册1000个插件时,我们自己手写的 MySyncHook执行时间0.12ms,而官方的需要6ms,这中间整整50倍的差距!!!
具体可以看我的仓库
The text was updated successfully, but these errors were encountered: