服务粉丝

我们一直在努力
当前位置:首页 > 财经 >

webpack 的 scope hositing 实现原理,它也是一种 tree shaking!

日期: 来源:神光的编程秘籍收集编辑:神说要有光zxg

scope hoisting 是 webpack 3 就已经实现的功能了,它能优化生成代码的性能,还能实现部分 tree shaking。

那什么是 scope hoisting,它又是怎么实现的呢?

我们一起来看一下:

这样一个 example.js 的入口模块,它引用了 a 模块:

a 模块导出了一个 a 变量,又引入了 x 模块:

x 模块导出了 x 变量,引入了 y 模块:

y 模块导出了 y 变量:

就是一个模块的引用链条,比较容易搞懂:

有这样一个 webpack 配置文件:

开发模式,不生成 sourcemap,入口是 example.js。

生成的代码是这样的:

a、x、y 都被一个函数包裹:

然后在入口模块引入:

这个很正常,因为浏览器里实现模块就是用函数的方式嘛。

这是没有开启 scope hositing 的时候。

我们接下来开启 scope hositing 看看会发生什么。

开启的话加一个 optimization.concatenateModules 为 true 的配置就好了:

concatenateModules 是连接模块的意思。

重新执行 webpack,这时候生成的代码是这样的:

a、x、y 模块的代码被合并到了一个函数作用域里。

这就是 scope hoisting 的功能。

有的同学说,这样合并有问题的吧,如果有同名变量怎么办?

我们试一下:

在 a 模块定义了一个 aa 变量:

在 x 模块也定义了一个 aa 变量:

重新跑下 webpack,生成的代码是这样的:

可以看到同名的变量被加上文件名的前缀(上面一张图重复了忘删了):

这样同名变量经过重命名后,就不再会冲突了。

那 scope hoisting 有什么好处呢?

很容易想到的是性能的提升,本来要创建好几个函数的闭包,因为被别的函数作用域引用嘛,现在只需要创建一个了,占据的内存会更小。

再就天然能实现 tree shaking。

比如 x 模块里导出了一个 x2 变量,这个变量没有被使用:

不开启 scope hositing 的时候,生成的代码是这样的,它会被导出,只是没被使用:

这时候如果你要删掉它,你需要分析模块之间的依赖关系,导出的变量哪些被使用了,哪些没被使用。

还要保证这段代码没有副作用,才能把它删除掉。

也就是 tree shaking 掉。

但如果 scope hositing 了之后呢?

这时你能很容易的分析出变量引用关系,然后把它删掉。

这个都不用 webpack 做,直接用 TerserWebpackPlugin 这种压缩的插件来做就行。

所以说,当实现了 scope hositing 之后,天然就支持了部分模块的 tree shaking。

为什么说是部分呢?

因为 scope hositing 也是有限制条件的。

可以在文档里看到这些,叫做 optimization bailouts,优化的退出条件,意思就是这些情况下不会做 scope hositing:

一个个来看:

Non ES6 Module 不会做 scope hoisting。

这个很容易理解,只有 es module 的依赖关系才是能被分析的,都不是 es module 怎么正确分析依赖关系,怎么 hositing 呢?

export * from "cjs-module" 不会做 scope hositing

这个同上,一旦模块引入了 cjs module,那就不可以分析依赖关系了,所以也就不能 hoisting。

use eval() 不会做 scope hositing

这个也容易理解,eval 的代码你不能保证有啥东西,去掉 scope,合在一起很容易出问题。

using module 或者用了 ProvidePlugin 的变量,不会被 scope hositing。

用到了 module 变量之后,你合并成了一个模块,那这个 module 不就没了么?

所以用了 module 变量不会被 hositing。

用了 ProvidePlugin 注入的变量也差不多。

In Multiple Chunks 不会被 scope hositing。

要是被多个 chunk 用到了,那 hositing 之后,代码不就重复了多次么?

所以只有被一个 chunk 用到的模块才会被 hositing 优化。

这些不会触发 scope hositing 的情况倒是都挺容易理解的。

我们挑几个来试一下:

比如我在 y 模块用一个 eval:

跑下 webpack,这时生成的代码是这样的:

其他 3 个模块都被 scope hositing 了,就是这个 y 变成了从别的模块引入的方式。

可以看到在上面单独定义了 y 模块:

这就是规则里说的,有 eval 的模块不会触发 scope hositing。

我们再来看看被多 chunk 引入的情况:

添加这样一个 lazy 模块,引入 x 模块:

然后在 example 里异步引入它:

异步引入的模块是会被分到单独的 chunk 的。

我们重新跑下 webpack 试试。

确实,lazy 的模块单独分了一个 chunk:

因为 x 被 lazy 引用了,而 y 被 x 引用。

所以 scope hositing 是这样做的:

example 和 a 被 hositing 到一个模块了,而 x 单独引入的。

而这个 x 模块里把 x、y 给 hosting 成一个模块了:

这就是模块在多个 chunk 时,会把它单独摘出来,不会被 scope hositing。

知道了什么是 scope hositing,什么时候会触发 scope hositing,哪些情况不会。

我们再来看看它的实现原理。

其实这个 optimization.concatenateModules 的配置不用自己开启:

当你把 mode 设置为 production 的时候,默认就会开启这个选项。

而开启这个选项的时候,内部会应用 ModuleConcatenationPlugin 这个插件:

也就是说 scope hositing 的功能就是 ModuleConcatenationPlugin 这个插件实现的。

webpack 的流程分为 3 步:

make 是从入口模块开启,递归解析依赖,生成模块依赖图 ModuleGraph。

seal 阶段是把 module 分到不同的 chunk,也就是分组,生成 ChunkGraph。

emit 阶段把每个 chunk 使用模版打印出来,生成代码,也就是 assets。

之后把 assets 写入磁盘就好了。

ModuleConcatenationPlugin 这个插件在 optimizeChunkModules 这个 hook 生效,

这是 seal 阶段的一个 hook:

这时候 moduleGraph、chunkGraph 都有了,可以从 compilation 对象里拿到。

这个插件逻辑还是比较复杂的,我们理一下主流程好了:

这个插件会遍历所有模块,把不适合 scope hositing 的模块过滤掉,同时记录下不合适的原因。

最后剩下的有的是入口模块,有的是其他的可以被 scope hositing 的模块,分别放到两个集合中:

然后从每个入口模块开始,递归分析 import,把可以被 scope hositing 的模块都放到这个 ConcatConfiguration 对象里:

这个对象的作用就是记录根模块和子模块:

这是一个可以被 scope hositing 的单位。

然后它会遍历这些配置对象来创建一个个新的 module 对象:

这些新的 module 对象包含的子 module 都是可以被一起 scope hositing 的。

这是一个继承了 webpack 的 Module 类的特殊的 Module 类。

然后把这个 module 包含的子 module 从之前的 chunk 里删掉,

之后把这个 module 替换成新的 module。

替换成 ConcatenatedModule 类型的新 module 对象有什么用呢?

作用在代码生成阶段。

代码生成的时候会调用每个 module 对象的 codeGeneration 方法:

而这个 module 对象是我们前面替换的 ConcatenatedModule 类型的,它重写了 codeGeneration 方法

会遍历模块,根据类型分别打上 concatenated 和 external 的标记:

也就是是单独一个模块,还是合并到一起。

之后拼接代码字符串的时候就会根据不同的类型做不同的处理。

对 concatenated 类型的模块,还会对每个顶层的变量通过 AST 查找是否有同名变量,有的话就重命名。

拼接代码的时候也是用不同的模版:

上面这段字符串是不是觉得眼熟?

没错,这部分就是在拼接最终生成的这种代码:

可以对比下:

这样我们就走完了这个插件的逻辑还有最终代码生成的逻辑。

这就是 scope hoisting 的实现原理。

总结

scope hosiitng 可以把一些模块的代码合并成一个模块作用域里,这样性能会更高,而且配合压缩插件就可以实现 tree shaking。

同名的变量也不用担心,scope hositing 的时候会做重命名。

当然,也不是所有的模块都可以 scope hositing,有一些模块不可以,主要是被多个 chunk 包含的模块、有 cjs 代码的模块、有 eval 的模块、用到了 module 变量的模块。

这些类型的模块不能被 scope hoisting 的原因也很容易理解,比如 cjs、eval 的代码不能被分析、被多个 chunk 包含的模块如果 hositing 会重复等等。

scope hositing 的功能需要开启 optimization.concatenateModules 的配置项,或者设置 mode 为 production,它的底层就是 ModuleConcatenationPlugin 这个插件。

webpack 分为 make、seal、emit 3个阶段,这个插件在 seal 阶段的 optimizeChunkModules 的 hook 生效。

它会遍历模块,根据规则过滤出可以被 scope hositing 的入口模块和其他模块,放到一个 ConcatConfiguration 对象里。

然后遍历这个对象,生成 ConcatenatedModule 类型的 module 替换之前的 module。

这样当代码生成阶段,就会调用 ConcatenatedModule 的 codeGeneration 方法,这里做了模块类型的区分,同名变量的重命名,以及最终模块代码的拼接。

这样生成的代码就是 scope hositing 的代码了。

这就是 scope hositing 的实现原理,它是 webpack 的基础功能之一,而且也实现了部分 tree shaking。

相关阅读

  • 13 个你可能未使用过的 Python 技巧!

  • 来源丨关于数据分析与可视化大多数程序员不知道的令人难以置信的功能列表。Python 是顶级编程语言之一,它具有许多程序员从未使用过的许多隐藏功能。在这篇博客中,我将分享你
  • 这个常见动作致人死亡!被判9个月!千万注意

  • 开车门是司机上下车必然会做的一个动作,但如果不够小心,很可能就会上演“开门杀”,酿成事故。近日,江苏如东法院开庭审理了一起因开车门致人伤亡的交通肇事案件,被告人吴某犯交通
  • 气象兵,到!

  • 文、图 | 晏军敏、李伟辉、华禧一观察风云雷电预测阴晴雨雪有那么一群人他们站在星夜里胸怀经纬测风云他们时刻掌握飞行训练进度与安全把牢飞行训练保障的第一道关口他们就
  • 每日速递|多氟多目标2024年钠电正极产能7000吨

  • ◆电池◆01欣旺达正在开发钠离子电池3月23日,欣旺达在互动平台表示,相比于锂电池,钠电池的能量密度较低,适合应用于低速电动车和储能等领域,公司认为,钠电池是对锂电池的一种补充,
  • 菌说|大学“牲”解压迷惑行为大赏

  • 大学“牲”的生活如同升级打怪,总要面对层出不穷的压力。堆积的压力不仅会让自己心情低落,长此以往也不利于心理健康。那么,面对生活中的小颓丧,如何释放心中的压力和负能量呢?菌
  • @全球武大校友,母校呼叫你!

  • 启程珞珈山下传遍五洲寰宇看,在九一二体育场上正飘扬着一面校旗它即将飘洋过海给五湖四海的武大人送去“百卅”校庆的邀约@全球武大校友母校向你发出召唤啦伴着校歌的旋律国
  • 让自己越来越有价值的几种能力

  • 真正决定一个人价值的,不是学历,而是你的不可代替性。所以如何让我们越来越值钱,就在于你有没有能力让自己成为那个不可代替的人,脱颖而出。以下是能让你越来越有价值的能力,欢迎
  • 免费!三亚今年将为适龄女生接种HPV疫苗

  • 日前,《2023年三亚市适龄女生HPV疫苗接种项目实施方案》(以下简称《方案》)印发,将对全市5个区,2023年学籍在该市七年级及以上在校女生(第1剂次接种年龄满13周岁至14周岁半)免费接

热门文章

  • “复活”半年后 京东拍拍二手杀入公益事业

  • 京东拍拍二手“复活”半年后,杀入公益事业,试图让企业捐的赠品、家庭闲置品变成实实在在的“爱心”。 把“闲置品”变爱心 6月12日,“益心一益·守护梦想每一步”2018年四

最新文章

  • 新增9个千亿县,江苏何以独揽4席

  • 全国GDP千亿县继续扩围。澎湃新闻梳理发现,2022年,全国千亿县数量达52个,较2021年增加9个,昆山、江阴、张家港继续领跑,其中,昆山成为全国首个GDP破5000亿元的县级市,达5006.7亿元
  • 星巴克“颠覆”星巴克:冲破第三空间

  • 来源丨全天候科技作者丨胡描图源丨摄图网经过长达一年的酝酿和测试,星巴克中国与高德地图合作的“沿街取”服务,在北京、上海两座城市的150家门店中正式上线。星巴克计划在未