上文提到通过构建工具设置预定义的常量 __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 如何工作,其目录结构如下:
├── demo
│ └── package.json
│ └── input.js
│ └── utils.js
首先安装 rollup.js:
yarn add rollup -D
# 或 npm install rollup -D
下面是 input.js 和 utils.js 文件的内容:
import { foo } from './utils.js'
foo()
export function foo(obj) {
obj && obj.foo
}
export function bar(obj) {
obj && obj.bar
}
代码很简单,我们在 utils.js 文件中定义并导出了两个函数,分别是 foo 函数和 bar 函数, 然后在 input.js 中导入了 foo 函数并执行。注意,我们并没有导入 bar 函数。
接着,我们执行如下命令进行构建:
npx rollup input.js -f esm -o bundle.js
这句命令的意思是,以 input.js 文件为入口,输出 ESM,输出的文件叫作 bundle.js。命令执行成功后,我们打开 bundle.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 文件:
import { foo } from './utils'
/*#__PURE__*/ foo()
注释代码 /*#__PURE__*/
,其作用就是告诉 rollup.js,对于 foo 函数的调用不会产生副作用,你可以放心地对其进行 Tree-Shaking, 此时再次执行构建命令并查看 bundle.js 文件,就会发现它的内容是空的,这说明 Tree-Shaking 生效了。
基于这个案例,我们应该明白,在编写框架的时候需要合理使用 /*#__PURE__*/
注释。如果你去搜索 Vue.js 3 的源码,会发现它大量使用了该注释,例如下面这句:
export const isHTMLTag = /*#__PURE__*/ makeMap(HTML_TAGS)
这会不会对编写代码造成很大的心智负担呢?其实不会,因为通常产生副作用的代码都是模块内函数的顶级调用。什么是顶级调用呢?如以下代码所示:
foo() // 顶级调用
function bar() {
foo() // 函数内调用
}
可以看到,对于顶级调用来说,是可能产生副作用的;但对于函数内调用来说,只要函数 bar 没有被调用,那么 foo 函数的调用自然不会产生副作用。 因此,在 Vue.js 3 的源码中,基本都是在一些顶级调用的函数上使用 /*#__PURE__*/
注释。当然,注释代码不仅仅作用于函数,它可以应用于任何语句上。 该注释也不是只有 rollup.js 才能识别,webpack 以及压缩工具(如 terser)都能识别它。