2024年Web前端最新常见面试题:Tree-Shaking 实现原理,美团 前端 面经

最后

整理面试题,不是让大家去只刷面试题,而是熟悉目前实际面试中常见的考察方式和知识点,做到心中有数,也可以用来自查及完善知识体系。

开源分享:【大厂前端面试题解析+核心总结学习笔记+真实项目实战+最新讲解视频】

《前端基础面试题》,《前端校招面试题精编解析大全》,《前端面试题宝典》,《前端面试题:常用算法》

前端面试题宝典

前端校招面试题详解

import {bar} from ‘./bar’;

console.log(bar);

// bar.js

export const bar = ‘bar’;

export const foo = ‘foo’;

示例中,bar.js 模块导出了 barfoo ,但只有 bar 导出值被其它模块使用,经过 Tree Shaking 处理后,foo 变量会被视作无用代码删除。

二、实现原理

======

Webpack 中,Tree-shaking 的实现一是先**「标记」**出模块导出值中哪些没有被用过,二是使用 Terser 删掉这些没被用到的导出语句。标记过程大致可划分为三个步骤:

  • Make 阶段,收集模块导出变量并记录到模块依赖关系图 ModuleGraph 变量中

  • Seal 阶段,遍历 ModuleGraph 标记模块导出变量有没有被使用

  • 生成产物时,若变量没有被其它模块使用则删除对应的导出语句

标记功能需要配置 optimization.usedExports = true 开启

也就是说,标记的效果就是删除没有被其它模块使用的导出语句,比如:

示例中,bar.js 模块(左二)导出了两个变量:barfoo,其中 foo 没有被其它模块用到,所以经过标记后,构建产物(右一)中 foo 变量对应的导出语句就被删除了。作为对比,如果没有启动标记功能(optimization.usedExports = false 时),则变量无论有没有被用到都会保留导出语句,如上图右二的产物代码所示。

注意,这个时候 foo 变量对应的代码 const foo='foo' 都还保留完整,这是因为标记功能只会影响到模块的导出语句,真正执行“「Shaking」”操作的是 Terser 插件。例如在上例中 foo 变量经过标记后,已经变成一段 Dead Code —— 不可能被执行到的代码,这个时候只需要用 Terser 提供的 DCE 功能就可以删除这一段定义语句,以此实现完整的 Tree Shaking 效果。

接下来我会展开标记过程的源码,详细讲解 Webpack 5 中 Tree Shaking 的实现过程,对源码不感兴趣的同学可以直接跳到下一章。

2.1 收集模块导出


首先,Webpack 需要弄清楚每个模块分别有什么导出值,这一过程发生在 make 阶段,大体流程:

关于 Mak e 阶段的更多说明,请参考前文 [万字总结] 一文吃透 Webpack 核心原理

  1. 将模块的所有 ESM 导出语句转换为 Dependency 对象,并记录到 module 对象的 dependencies 集合,转换规则:
  • 具名导出转换为 HarmonyExportSpecifierDependency 对象

  • default 导出转换为 HarmonyExportExpressionDependency 对象

例如对于下面的模块:

export const bar = ‘bar’;

export const foo = ‘foo’;

export default ‘foo-bar’

对应的dependencies 值为:

  1. 所有模块都编译完毕后,触发 compilation.hooks.finishModules 钩子,开始执行 FlagDependencyExportsPlugin 插件回调

  2. FlagDependencyExportsPlugin 插件从 entry 开始读取 ModuleGraph 中存储的模块信息,遍历所有 module 对象

  3. 遍历 module 对象的 dependencies 数组,找到所有 HarmonyExportXXXDependency 类型的依赖对象,将其转换为 ExportInfo 对象并记录到 ModuleGraph 体系中

经过 FlagDependencyExportsPlugin 插件处理后,所有 ESM 风格的 export 语句都会记录在 ModuleGraph 体系内,后续操作就可以从 ModuleGraph 中直接读取出模块的导出值。

参考资料:

  1. [万字总结] 一文吃透 Webpack 核心原理
  1. 有点难的 webpack 知识点:Dependency Graph 深度解析

2.2 标记模块导出


模块导出信息收集完毕后,Webpack 需要标记出各个模块的导出列表中,哪些导出值有被其它模块用到,哪些没有,这一过程发生在 Seal 阶段,主流程:

  1. 触发 compilation.hooks.optimizeDependencies 钩子,开始执行 FlagDependencyUsagePlugin 插件逻辑

  2. FlagDependencyUsagePlugin 插件中,从 entry 开始逐步遍历 ModuleGraph 存储的所有 module 对象

  3. 遍历 module 对象对应的 exportInfo 数组

  4. 为每一个 exportInfo 对象执行 compilation.getDependencyReferencedExports 方法,确定其对应的 dependency 对象有否被其它模块使用

  5. 被任意模块使用到的导出值,调用 exportInfo.setUsedConditionally 方法将其标记为已被使用。

  6. exportInfo.setUsedConditionally 内部修改 exportInfo._usedInRuntime 属性,记录该导出被如何使用

  7. 结束

上面是极度简化过的版本,中间还存在非常多的分支逻辑与复杂的集合操作,我们抓住重点:标记模块导出这一操作集中在 FlagDependencyUsagePlugin 插件中,执行结果最终会记录在模块导出语句对应的 exportInfo._usedInRuntime 字典中。

2.3 生成代码


经过前面的收集与标记步骤后,Webpack 已经在 ModuleGraph 体系中清楚地记录了每个模块都导出了哪些值,每个导出值又没那块模块所使用。接下来,Webpack 会根据导出值的使用情况生成不同的代码,例如:

重点关注 bar.js 文件,同样是导出值,barindex.js 模块使用因此对应生成了 __webpack_require__.d 调用 "bar": ()=>(/* binding */ bar),作为对比 foo 则仅仅保留了定义语句,没有在 chunk 中生成对应的 export。

关于 W ebpack 产物的内容及 __webpack_require__.d 方法的含义,可参考 Webpack 原理系列六:彻底理解 Webpack 运行时 一文。

这一段生成逻辑均由导出语句对应的 HarmonyExportXXXDependency 类实现,大体的流程:

  1. 打包阶段,调用 HarmonyExportXXXDependency.Template.apply 方法生成代码

  2. apply 方法内,读取 ModuleGraph 中存储的 exportsInfo 信息,判断哪些导出值被使用,哪些未被使用

  3. 对已经被使用及未被使用的导出值,分别创建对应的 HarmonyExportInitFragment 对象,保存到 initFragments 数组

  4. 遍历 initFragments 数组,生成最终结果

基本上,这一步的逻辑就是用前面收集好的 exportsInfo 对象未模块的导出值分别生成导出语句。

2.4 删除 Dead Code


经过前面几步操作之后,模块导出列表中未被使用的值都不会定义在 __webpack_exports__ 对象中,形成一段不可能被执行的 Dead Code 效果,如上例中的 foo 变量:

在此之后,将由 Terser、UglifyJS 等 DCE 工具“摇”掉这部分无效代码,构成完整的 Tree Shaking 操作。

2.5 总结


综上所述,Webpack 中 Tree Shaking 的实现分为如下步骤:

  • FlagDependencyExportsPlugin 插件中根据模块的 dependencies 列表收集模块导出值,并记录到 ModuleGraph 体系的 exportsInfo

  • FlagDependencyUsagePlugin 插件中收集模块的导出值的使用情况,并记录到 exportInfo._usedInRuntime 集合中

  • HarmonyExportXXXDependency.Template.apply 方法中根据导出值的使用情况生成不同的导出语句

  • 使用 DCE 工具删除 Dead Code,实现完整的树摇效果

上述实现原理对背景知识要求较高,建议读者同步配合以下文档食用:

  1. [万字总结] 一文吃透 Webpack 核心原理
  1. 有点难的 webpack 知识点:Dependency Graph 深度解析
  1. Webpack 原理系列六:彻底理解 Webpack 运行时

三、最佳实践

======

虽然 Webpack 自 2.x 开始就原生支持 Tree Shaking 功能,但受限于 JS 的动态特性与模块的复杂性,直至最新的 5.0 版本依然没有解决许多代码副作用带来的问题,使得优化效果并不如 Tree Shaking 原本设想的那么完美,所以需要使用者有意识地优化代码结构,或使用一些补丁技术帮助 Webpack 更精确地检测无效代码,完成 Tree Shaking 操作。

3.1 避免无意义的赋值


使用 Webpack 时,需要有意识规避一些不必要的赋值操作,观察下面这段示例代码:

示例中,index.js 模块引用了 bar.js 模块的 foo 并赋值给 f 变量,但后续并没有继续用到 foof 变量,这种场景下 bar.js 模块导出的 foo 值实际上并没有被使用,理应被删除,但 Webpack 的 Tree Shaking 操作并没有生效,产物中依然保留 foo 导出:

造成这一结果,浅层原因是 Webpack 的 Tree Shaking 逻辑停留在代码静态分析层面,只是浅显地判断:

  • 模块导出变量是否被其它模块引用

  • 引用模块的主体代码中有没有出现这个变量

没有进一步,从语义上分析模块导出值是不是真的被有效使用。

更深层次的原因则是 JavaScript 的赋值语句并不**「纯」**,视具体场景有可能产生意料之外的副作用,例如:

import { bar, foo } from “./bar”;

let count = 0;

const mock = {}

Object.defineProperty(mock, ‘f’, {

set(v) {

mock._f = v;

count += 1;

}

})

mock.f = foo;

console.log(count);

最后

文章到这里就结束了,如果觉得对你有帮助可以点个赞哦

开源分享:【大厂前端面试题解析+核心总结学习笔记+真实项目实战+最新讲解视频】

import { bar, foo } from “./bar”;

let count = 0;

const mock = {}

Object.defineProperty(mock, ‘f’, {

set(v) {

mock._f = v;

count += 1;

}

})

mock.f = foo;

console.log(count);

最后

文章到这里就结束了,如果觉得对你有帮助可以点个赞哦

开源分享:【大厂前端面试题解析+核心总结学习笔记+真实项目实战+最新讲解视频】

[外链图片转存中…(img-acy2HpD7-1715462062598)]

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值