Skip to content

上文提到通过构建工具设置预定义的常量 __DEV__,就能够在生产环境中使得框架不包含用于打印警告信息的代码, 从而使得框架自身的代码量不随警告信息的增加而增加。但是从用户的角度来看,这么做仍然不够,还是拿 Vue.js 来举个例子。 我们知道 Vue.js 内建了很多组件,例如 <Transition> 组件,如果我们的项目中根本就没有用到该组件,那么它的代码需要包含在项目最终的构建资源中吗? 答案是“当然不需要”,那么如何做到这一点呢?这就不得不提到本节的主角 Tree-Shaking。

什么是 Tree-Shaking 呢?在前端领域,这个概念因 rollup.js 而普及。简单地说, Tree-Shaking 指的就是消除那些永远不会被执行的代码,也就是排除 dead code(僵尸代码),现在无论是 rollup.js 还是 webpack,都支持 Tree-Shaking。

想要实现 Tree-Shaking,必须满足一个条件,即模块必须是 ESM(ES Module),因为 Tree-Shaking 依赖 ESM 的静态结构。 我们以 rollup.js 为例看看 Tree-Shaking 如何工作,其目录结构如下:

md
├── demo
│   └── package.json
│   └── input.js
│   └── utils.js

首先安装 rollup.js:

shell
yarn add rollup -D
# 或 npm install rollup -D

下面是 input.js 和 utils.js 文件的内容:

js
import { foo } from './utils.js'
foo()
js
export function foo(obj) {
  obj && obj.foo
}
export function bar(obj) {
  obj && obj.bar
}

代码很简单,我们在 utils.js 文件中定义并导出了两个函数,分别是 foo 函数和 bar 函数, 然后在 input.js 中导入了 foo 函数并执行。注意,我们并没有导入 bar 函数。

接着,我们执行如下命令进行构建:

shell
npx rollup input.js -f esm -o bundle.js

这句命令的意思是,以 input.js 文件为入口,输出 ESM,输出的文件叫作 bundle.js。命令执行成功后,我们打开 bundle.js 来查看一下它的内容:

js
// bundle.js
function foo(obj) {
  obj && obj.foo
}
foo()

可以看到,其中并不包含 bar 函数,这说明 Tree-Shaking 起了作用。由于我们并没有使用 bar 函数,因此它作为 dead code 被删除了。 但是仔细观察会发现,foo 函数的执行也没有什么意义,仅仅是读取了对象的值,所以它的执行似乎没什么必要。 既然把这段代码删了也不会对我们的应用程序产生影响,那么为什么 rollup.js 不把这段代码也作为 dead code 移除呢?

这就涉及 Tree-Shaking 中的第二个关键点——副作用。如果一个函数调用会产生副作用,那么就不能将其移除。什么是副作用? 简单地说,副作用就是,当调用函数的时候会对外部产生影响,例如修改了全局变量。这时你可能会说,上面的代码明显是读取对象的值,怎么会产生副作用呢? 其实是有可能的,试想一下,如果 obj 对象是一个通过 Proxy 创建的代理对象,那么当我们读取对象属性时,就会触发代理对象的 get 夹子(trap), 在 get 夹子中是可能产生副作用的,例如我们在 get 夹子中修改了某个全局变量。而到底会不会产生副作用,只有代码真正运行的时候才能知道, JS 本身是动态语言,因此想要静态地分析哪些代码是 dead code 很有难度,上面只是举了一个简单的例子。

因为静态地分析 JS 代码很困难,所以像 rollup.js 这类工具都会提供一个机制,让开发者能明确地告诉 rollup.js:“放心吧, 这段代码不会产生副作用,你可以移除它。”具体怎么做呢?如以下代码所示,我们修改 input.js 文件:

js
import { foo } from './utils'

/*#__PURE__*/ foo()

注释代码 /*#__PURE__*/,其作用就是告诉 rollup.js,对于 foo 函数的调用不会产生副作用,你可以放心地对其进行 Tree-Shaking, 此时再次执行构建命令并查看 bundle.js 文件,就会发现它的内容是空的,这说明 Tree-Shaking 生效了。

基于这个案例,我们应该明白,在编写框架的时候需要合理使用 /*#__PURE__*/ 注释。如果你去搜索 Vue.js 3 的源码,会发现它大量使用了该注释,例如下面这句:

js
export const isHTMLTag = /*#__PURE__*/ makeMap(HTML_TAGS)

这会不会对编写代码造成很大的心智负担呢?其实不会,因为通常产生副作用的代码都是模块内函数的顶级调用。什么是顶级调用呢?如以下代码所示:

js
foo() // 顶级调用

function bar() {
  foo() // 函数内调用
}

可以看到,对于顶级调用来说,是可能产生副作用的;但对于函数内调用来说,只要函数 bar 没有被调用,那么 foo 函数的调用自然不会产生副作用。 因此,在 Vue.js 3 的源码中,基本都是在一些顶级调用的函数上使用 /*#__PURE__*/ 注释。当然,注释代码不仅仅作用于函数,它可以应用于任何语句上。 该注释也不是只有 rollup.js 才能识别,webpack 以及压缩工具(如 terser)都能识别它

如有转载或CV请标注本站原文地址