哈哈笑了,根本难得倒我。
Tree Shaking 的核心思想是:
利用 ES6 的模块特性(静态导入),在编译阶段分析哪些模块和导出值没有被用到,然后在打包时将其剔除。
这主要依赖于两点:
-
ES Module(ES6 模块语法) :
import
/export
是静态结构,可以在编译时确定依赖关系; -
静态分析(Static Analysis) : Webpack 分析依赖图,只打包实际被使用的代码;
那么为什么 Webpack 可以在编译时确定依赖关系呢?这是个非常关键的问题。Webpack 之所以能够在编译时确定模块的依赖关系,是因为它依赖于 ES Module 的静态结构特性。
ES Module(import
/ export
)和 CommonJS(require
)的关键区别是:ES Module对依赖的加载是“静态”的,在编译时进行分析,但是CommonJS对依赖的加载是“动态”的,在运行时执行。
Webpack 如何做的(简要流程)
- 模块解析 Webpack 使用
acorn
、@babel/parser
等工具解析每个 JS 文件为抽象语法树(AST): - 依赖图构建 Webpack 根据 AST 构建一个模块依赖图(Module Dependency Graph),每个节点是一个模块,边是依赖关系。
- 标记导出是否使用 在构建时,Webpack 会从入口文件递归分析哪些导出被使用:被使用的模块标记为
usedExports = true
,没被使用的标记为usedExports = false
- 摇树优化(Tree Shaking) 在优化阶段,Webpack 根据
usedExports
利用 Terser(压缩插件) 剔除没被引用的导出。
那么AST是怎么构建的呢?
这个问题涉及编译器的核心原理,AST(Abstract Syntax Tree,抽象语法树) 是一种用树状结构表示源代码语法结构的数据形式。
在讲AST之前,我们先来了解一下一个完整的编译器整体执行过程:
- Parsing(解析过程) :这个过程要经
词法分析
、语法分析
、构建AST(抽象语法树)
一系列操作; - Transformation(转化过程):这个过程就是将上一步解析后的内容,按照编译器指定的规则进行处理,
形成一个新的表现形式
;(例如你想将JS转成C语言展示出来) - Code Generation(代码生成):将上一步处理好的内容
转化为新的代码
;
Babel 其实就是一个最常用的Javascript编译器,它能够转译 ECMAScript 2015+
的代码,使它在旧的浏览器或者环境中也能够运行。
整个 AST 构建过程分为三个主要阶段:
- 词法分析(Lexical Analysis): 把代码字符串转成一串Token(词法单元)
例如:
const a = 1 + 2;
会被拆成这些 token:
["const", "a", "=", "1", "+", "2", ";"]
这个阶段的目标是把字符流变成单词流,是语法分析的基础。
- 语法分析(Parsing): 把 token 流根据 JavaScript 的语法规则转换为一棵 AST,这一阶段使用上下文无关文法(CFG)构建语法树,描述每个结构的嵌套关系。
例如,变量声明 const a = 1 + 2
会被识别为:声明语句(VariableDeclaration)/初始化表达式(BinaryExpression)
这一步一般通过 递归下降解析器(Recursive Descent Parser) 或 LL/LR Parser 实现。
- 构建 AST(Abstract Syntax Tree): 将语法分析阶段得到的信息封装成树形结构。
每一个节点是一个对象,包含:type(节点类型)
,children
或相关属性。
Webpack 是怎么用 AST 的?
Webpack 在解析模块依赖时会:
- 调用
acorn
或@babel/parser
解析模块文件为 AST; - 遍历 AST,查找
ImportDeclaration
、ExportNamedDeclaration
节点; - 构建模块依赖图(Module Graph);
- 标记哪些导出值是“used”(用于 Tree Shaking);
- 最终传给 Terser 做 dead code elimination。
那么回到最初的问题:Webpack 是如何确定哪些模块是「被用到的(used exports) 」,从而实现 Tree Shaking 的呢?
Webpack 主要在 依赖图构建(ModuleGraph)和标记阶段(usedExports) 完成这个判断:
构建模块依赖图(ModuleGraph)
Webpack 先从入口文件出发,使用 acorn
或 @babel/parser
将 JS 源码解析为 AST。
然后遍历 AST:
// index.js
import { add } from './utils.js';
会生成一个节点:
{
"type": "ImportDeclaration",
"source": "./utils.js",
"specifiers": ["add"]
}
Webpack 记录:index.js
依赖 utils.js
的 add
。
分析模块的导出和引用(usedExports)
Webpack 从入口模块出发,递归地跟踪所有模块的导出与使用情况。
以这个 utils.js
为例:
export const add = (a, b) => a + b;
export const sub = (a, b) => a - b;
假设 index.js
只导入了 add
,Webpack 会标记:
{
"add": true,
"sub": false
}
这个过程的关键点是:ESM 是静态结构,Webpack 可以完全在编译阶段知道你用了哪些导出。
标记模块的导出是否被使用(usedExports)
Webpack 为每个模块都维护一个字段 usedExports
,记录哪些导出是“活的”:
module.usedExports = ['add']; // 或 true/false/all
这个字段会传递给后续 Webpack 优化插件,例如:TerserPlugin
会读取 usedExports
并把未使用的导出删掉。
总结:Webpack 如何知道模块是否被用?
Webpack 在构建阶段,通过分析每个模块的 AST,构建依赖图,并通过递归分析哪些导出被其他模块引用,来标记
usedExports
,从而实现 Tree Shaking。