一、概念层:什么是"绑定分析"(Binding Analysis)
在 Vue <script setup> 的编译过程中,每一个变量的绑定类型 都需要被识别、分类、并被记录下来。
编译器需要知道:
| 变量类型 | 对应运行时行为 |
|---|---|
| 普通常量(const) | 可直接访问,不需 unref |
| 响应式变量(ref/reactive) | 模板访问时需自动 unref |
| props 变量 | 由父组件传入、不可重写 |
| emits 函数 | 用于触发事件 |
| 宏生成绑定(如 defineModel) | 需要注入特定语义 |
这就是绑定分析(Binding Analysis)的职责 。
Vue 编译器用它来构建模板表达式的上下文模型,让模板编译器能在不执行代码的情况下理解哪些变量可响应、可修改、或是常量。
二、原理层:BindingTypes 枚举的定义
在 @vue/compiler-dom 中定义了 BindingTypes:
ini
export const enum BindingTypes {
DATA = 'data',
PROPS = 'props',
PROPS_ALIASED = 'props-aliased',
SETUP_LET = 'setup-let',
SETUP_CONST = 'setup-const',
SETUP_MAYBE_REF = 'setup-maybe-ref',
SETUP_REF = 'setup-ref',
SETUP_REACTIVE_CONST = 'setup-reactive-const',
LITERAL_CONST = 'literal-const',
}
简要说明:
| 类型 | 含义 |
|---|---|
| DATA | 传统 data() 返回的变量 |
| PROPS | defineProps() 声明的变量 |
| SETUP_LET | let 声明的变量(可变) |
| SETUP_CONST | const 声明的静态常量 |
| SETUP_MAYBE_REF | 常量但可能是 ref |
| SETUP_REF | 明确由 ref() 或 computed() 声明的响应式变量 |
| LITERAL_CONST | 字面量常量,如 const a = 123 |
| SETUP_REACTIVE_CONST | 来自 reactive() 或 defineModel() 的响应式对象 |
三、对比层:编译器推断 vs 运行时推断
Vue 的模板中可以直接访问 ref、reactive、props 等变量:
xml
<template>
<p>{{ count + 1 }}</p>
<button @click="count++">+</button>
</template>
<script setup>
const count = ref(0)
</script>
编译器需要决定:
模板中的
count是否需要.value?
这是通过 BindingTypes 来判断的:
- 如果是
SETUP_REF→ 编译器自动插入.value - 如果是
SETUP_CONST→ 直接访问 - 如果是
PROPS→ 编译器通过__props.count访问
这就是绑定分析的核心用途------
提前推断响应式行为,减少运行时代码判断。
四、实践层:绑定分析的执行位置
在 compileScript() 的后半部分,有这样一段关键逻辑:
ini
// analyze binding metadata
if (scriptAst) {
Object.assign(ctx.bindingMetadata, analyzeScriptBindings(scriptAst.body))
}
for (const [key, { isType, imported, source }] of Object.entries(ctx.userImports)) {
if (isType) continue
ctx.bindingMetadata[key] =
imported === '*' ||
(imported === 'default' && source.endsWith('.vue')) ||
source === 'vue'
? BindingTypes.SETUP_CONST
: BindingTypes.SETUP_MAYBE_REF
}
for (const key in scriptBindings) {
ctx.bindingMetadata[key] = scriptBindings[key]
}
for (const key in setupBindings) {
ctx.bindingMetadata[key] = setupBindings[key]
}
逐步解析:
-
普通
<script>绑定分析- 调用
analyzeScriptBindings()分析传统脚本; - 例如:
export const x = 1→SETUP_CONST
- 调用
-
导入语句绑定
- 分析所有
import; - 如果是
vue导入(如ref,computed),标记为SETUP_CONST; - 否则标记为
SETUP_MAYBE_REF。
- 分析所有
-
合并 setup 与 script 绑定
- 将
walkDeclaration()收集的结果写入; - 最终形成
ctx.bindingMetadata。
- 将
绑定分析的执行流程图
scss
┌──────────────────────────┐
│ <script setup> AST │
└────────────┬─────────────┘
│
walkDeclaration()
│
▼
┌──────────────────────────┐
│ setupBindings 生成 │
└────────────┬─────────────┘
│
analyzeScriptBindings()
│
▼
┌──────────────────────────┐
│ ctx.bindingMetadata 完成 │
└──────────────────────────┘
五、拓展层:walkDeclaration() 的递归推断逻辑
walkDeclaration() 是绑定分析的底层实现,它会递归遍历变量声明并判断绑定类型。
关键代码(节选)
ini
function walkDeclaration(from, node, bindings, userImportAliases, hoistStatic) {
if (node.type === 'VariableDeclaration') {
const isConst = node.kind === 'const'
for (const { id, init } of node.declarations) {
if (id.type === 'Identifier') {
let bindingType
if (isConst && isStaticNode(init!)) {
bindingType = BindingTypes.LITERAL_CONST
} else if (isCallOf(init, userImportAliases['ref'])) {
bindingType = BindingTypes.SETUP_REF
} else if (isCallOf(init, userImportAliases['reactive'])) {
bindingType = BindingTypes.SETUP_REACTIVE_CONST
} else if (isConst) {
bindingType = BindingTypes.SETUP_MAYBE_REF
} else {
bindingType = BindingTypes.SETUP_LET
}
registerBinding(bindings, id, bindingType)
}
}
}
}
逐行解释:
-
检测声明类型:
如果是
const→ 可能是常量;如果是
let→ 可能是可变绑定。 -
判断右值类型:
- 调用
isStaticNode(init)检查是否是纯字面量; - 如果是
ref()调用 → 标记为SETUP_REF; - 如果是
reactive()调用 → 标记为SETUP_REACTIVE_CONST。
- 调用
-
注册绑定类型:
registerBinding(bindings, id, bindingType)负责写入到bindings映射表中。
🧠 编译器通过静态语义分析即可精确判断响应式特征,无需运行时探测。
六、潜在问题与设计权衡
| 问题 | 说明 |
|---|---|
| 类型模糊性 | 复杂表达式(如 const a = cond ? ref(1) : 2)无法准确推断。 |
| 动态导入推断困难 | import() 或 require() 不在静态作用域中,跳过分析。 |
| 宏嵌套复杂度 | 宏中声明的变量(如 defineModel())需在宏解析后再标注。 |
| 性能考虑 | 对大型 AST 进行递归遍历需要优化(Vue 使用了缓存与类型短路)。 |
七、小结
通过本篇我们了解了:
- BindingTypes 的定义与作用
- 绑定分析的执行流程与源码结构
- walkDeclaration 的递归推断逻辑
- 模板编译中如何利用这些绑定信息生成优化的渲染函数
至此,我们已理解 <script setup> 编译的"语义层"基础。
接下来在 第 4 篇 ,我们将探讨 Vue 如何合并普通 <script> 与 <script setup> 的逻辑 ,
并解释为什么默认导出会被重写成 __default__。
本文部分内容借助 AI 辅助生成,并由作者整理审核。