一、概念层:AST 遍历在 Vue 编译中的地位
在 compileScript() 中,编译器需要识别以下几类变量声明:
csharp
const count = ref(0)
let name = 'Vue'
const { a, b } = reactive(obj)
Vue 必须在编译阶段理解这些声明,以判断:
- 哪些变量是响应式(
ref/reactive); - 哪些是常量;
- 哪些是 props;
- 哪些只是普通局部变量。
这一分析的关键函数就是:
scss
walkDeclaration()
它负责递归遍历 AST 节点,推断每个绑定的类型,并将结果注册进 bindings 映射表中。
二、原理层:walkDeclaration 的核心逻辑
原函数签名如下:
php
function walkDeclaration(
from: 'script' | 'scriptSetup',
node: Declaration,
bindings: Record<string, BindingTypes>,
userImportAliases: Record<string, string>,
hoistStatic: boolean,
isPropsDestructureEnabled = false,
): boolean
参数说明:
| 参数 | 含义 |
|---|---|
from |
当前声明来源(普通脚本或 setup 脚本) |
node |
当前 AST 声明节点(VariableDeclaration / FunctionDeclaration 等) |
bindings |
存储当前作用域的变量绑定类型 |
userImportAliases |
用于识别 ref / reactive 的真实导入名 |
hoistStatic |
是否允许静态常量上移 |
isPropsDestructureEnabled |
是否启用 props 解构优化 |
函数整体结构:
go
if (node.type === 'VariableDeclaration') {
// 1. 遍历变量声明
} else if (node.type === 'TSEnumDeclaration') {
// 2. TypeScript 枚举声明
} else if (node.type === 'FunctionDeclaration' || node.type === 'ClassDeclaration') {
// 3. 函数与类声明
}
输出:返回一个布尔值 isAllLiteral,用于判断该声明是否为纯字面量常量。
三、对比层:walkDeclaration 与 analyzeScriptBindings 的区别
| 函数 | 工作层级 | 分析内容 | 用途 |
|---|---|---|---|
analyzeScriptBindings |
顶层 | 快速扫描 script 区域的导出绑定 |
普通脚本变量分析 |
walkDeclaration |
递归层 | 深入遍历 setup 变量结构 | 组合式变量分析 |
前者是全局扫描(只看顶层变量),
后者是递归遍历模式结构(处理解构、数组、对象、函数等复杂绑定)。
四、实践层:walkDeclaration 主体流程详解
1️⃣ 处理普通变量声明
ini
if (node.type === 'VariableDeclaration') {
const isConst = node.kind === 'const'
isAllLiteral =
isConst &&
node.declarations.every(
decl => decl.id.type === 'Identifier' && isStaticNode(decl.init!),
)
for (const { id, init: _init } of node.declarations) {
const init = _init && unwrapTSNode(_init)
if (id.type === 'Identifier') {
let bindingType
if (isConst && isStaticNode(init!)) {
bindingType = BindingTypes.LITERAL_CONST
} else if (isCallOf(init, userImportAliases['reactive'])) {
bindingType = BindingTypes.SETUP_REACTIVE_CONST
} else if (isCallOf(init, userImportAliases['ref'])) {
bindingType = BindingTypes.SETUP_REF
} else if (isConst) {
bindingType = BindingTypes.SETUP_MAYBE_REF
} else {
bindingType = BindingTypes.SETUP_LET
}
registerBinding(bindings, id, bindingType)
}
}
}
🧩 步骤拆解:
| 步骤 | 操作 | 说明 |
|---|---|---|
| ① | 判断 const / let |
影响变量是否可变 |
| ② | 调用 isStaticNode(init) |
判断右值是否为纯字面量(例如数字、字符串) |
| ③ | 调用 isCallOf(init, userImportAliases['ref']) |
检测是否调用了 ref()、reactive() |
| ④ | 确定绑定类型 | 通过逻辑树推断对应的 BindingTypes |
| ⑤ | 注册绑定 | 调用 registerBinding(bindings, id, bindingType) 存入记录表 |
2️⃣ 处理解构模式:walkObjectPattern 与 walkArrayPattern
Vue 的 setup 可能出现这种写法:
scss
const { x, y } = usePoint()
const [a, b] = list
编译器需要递归解析这些模式结构。
对象模式:
scss
function walkObjectPattern(node, bindings, isConst, isDefineCall) {
for (const p of node.properties) {
if (p.type === 'ObjectProperty') {
if (p.key === p.value) {
registerBinding(bindings, p.key, BindingTypes.SETUP_MAYBE_REF)
} else {
walkPattern(p.value, bindings, isConst, isDefineCall)
}
} else {
// ...rest 参数
registerBinding(bindings, p.argument, BindingTypes.SETUP_LET)
}
}
}
数组模式:
scss
function walkArrayPattern(node, bindings, isConst, isDefineCall) {
for (const e of node.elements) {
if (e) walkPattern(e, bindings, isConst, isDefineCall)
}
}
3️⃣ 递归统一入口:walkPattern
ini
function walkPattern(node, bindings, isConst, isDefineCall = false) {
if (node.type === 'Identifier') {
registerBinding(bindings, node, isConst ? BindingTypes.SETUP_MAYBE_REF : BindingTypes.SETUP_LET)
} else if (node.type === 'ObjectPattern') {
walkObjectPattern(node, bindings, isConst)
} else if (node.type === 'ArrayPattern') {
walkArrayPattern(node, bindings, isConst)
} else if (node.type === 'AssignmentPattern') {
walkPattern(node.left, bindings, isConst)
}
}
关键点说明:
- 递归调用处理任意层级解构;
- 对默认值模式(
AssignmentPattern)同样递归; - 所有最终标识符都会落入
registerBinding()。
五、拓展层:静态节点检测与不可变优化
isStaticNode() 是 walkDeclaration 的辅助函数,用于检测一个表达式是否是纯常量。
java
function isStaticNode(node: Node): boolean {
switch (node.type) {
case 'StringLiteral':
case 'NumericLiteral':
case 'BooleanLiteral':
case 'NullLiteral':
case 'BigIntLiteral':
return true
case 'UnaryExpression':
return isStaticNode(node.argument)
case 'BinaryExpression':
return isStaticNode(node.left) && isStaticNode(node.right)
case 'ConditionalExpression':
return (
isStaticNode(node.test) &&
isStaticNode(node.consequent) &&
isStaticNode(node.alternate)
)
}
return false
}
这样,像下面的声明:
ini
const a = 100
const b = 1 + 2
都会被标记为 BindingTypes.LITERAL_CONST,
从而在渲染函数中跳过响应式追踪,提升性能。
六、潜在问题与性能考量
| 问题 | 说明 |
|---|---|
| 复杂模式递归性能 | 深层嵌套解构可能造成递归栈压力(Vue 内部已优化为迭代式遍历)。 |
| TS 类型节点干扰 | TypeScript AST 节点会被包裹,需要通过 unwrapTSNode() 去除。 |
| 动态函数检测局限 | 若函数名被重命名(如 const r = ref),则无法识别。 |
| 宏嵌套干扰 | 宏(如 defineProps)中的变量会被另行处理,不能直接分析。 |
七、小结
通过本篇我们理解了:
walkDeclaration如何识别各种声明;- Vue 编译器如何用静态语义推断响应式特征;
walkObjectPattern/walkArrayPattern/walkPattern如何递归遍历结构;isStaticNode如何识别字面量常量。
walkDeclaration 是整个 <script setup> 编译器的"语义扫描仪",
它使 Vue 能在模板编译阶段就确定哪些变量需要响应式展开,哪些可以静态化。
下一篇(第 6 篇)我们将进入代码生成阶段,
📦 《代码生成与 SourceMap 合并:从编译结果到调试追踪》 ,
分析 genRuntimeProps()、genRuntimeEmits() 以及 mergeSourceMaps() 如何将模板与脚本拼接为最终可执行代码。
本文部分内容借助 AI 辅助生成,并由作者整理审核。