在 Vue 模板编译阶段,有一个特别重要的过程:把模板里的表达式转成可执行的 JS 代码。
比如模板里写:
css
<div>{{ count + 1 }}</div>
Vue 编译器会生成类似的渲染函数:
kotlin
return _ctx.count + 1
你写的只是 count,但编译器偷偷帮你加上了 _ctx.。
这一步加前缀 _ctx. 或 $props. 的过程,就叫 表达式重写(expression transform) 。
而负责干这事的,就是这个文件里的 transformExpression。
一、transformExpression 是干嘛的?
它的任务分两部分:
- 找到表达式节点(如
{{ ... }}、v-bind等) - 调用
processExpression来改写里面的代码
简单理解就是:
它扫描模板 AST(语法树),遇到表达式就去"加工"。
二、举个例子
假设你写了这个组件:
xml
<template>
<div>{{ count + 1 }}</div>
</template>
<script setup>
import { ref } from 'vue'
const count = ref(0)
</script>
模板里的 {{ count + 1 }} 看似简单,实际上 Vue 内部会:
- 用 Babel 把它解析成一个 JS 语法树;
- 找出每一个标识符(
count); - 判断这个变量是哪来的;
- 把它重写成正确的访问形式,比如
count.value。
最后渲染函数就变成:
kotlin
return count.value + 1
三、processExpression 是整个逻辑的核心
源码中 processExpression() 是整个文件的"大脑",
它会:
- 用 Babel 解析字符串表达式;
- 找出所有变量;
- 判断是不是
ref、props、setup变量; - 给变量加上前缀或
.value。
📍1. 解析表达式
ini
ast = parseExpression(source, { plugins: context.expressionPlugins })
Vue 用 Babel 的 @babel/parser 来把字符串 count + 1 转成一棵树,
这样它就能知道哪里是变量、哪里是操作符、哪里是函数。
📍2. 遍历变量(标识符)
javascript
walkIdentifiers(ast, (node, parent, _, isReferenced, isLocal) => {
// ...
})
这一步相当于"扫描"所有变量,比如:
count + msg
会找到两个标识符:count 和 msg。
📍3. 判断变量来源(bindingMetadata)
Vue 编译时会记录每个变量的"来源":
| 来源类别 | 标识 | 含义 | 重写结果 |
|---|---|---|---|
| setup 里的 ref | SETUP_REF |
响应式引用 | count.value |
| setup 常量 | SETUP_CONST |
普通变量 | count |
| props | PROPS |
传入属性 | __props.xxx |
| data/methods | 其他 | 实例上下文 | _ctx.xxx |
📍4. 真正的改写逻辑(rewriteIdentifier)
看这一段是关键👇:
typescript
if (type === BindingTypes.SETUP_REF) {
return `${raw}.value`
} else if (type === BindingTypes.PROPS) {
return genPropsAccessExp(raw) // => __props.xxx
} else if (type && type.startsWith('setup')) {
return `$setup.${raw}`
} else {
return `_ctx.${raw}`
}
意思就是:
- 如果这个变量是个
ref→ 加.value - 如果是
props→ 改成__props.xxx - 如果是 setup 变量 → 改成
$setup.xxx - 其他情况 → 加
_ctx.
📍5. 拼回去生成最终表达式
举个例子:
{{ count + msg }}
经过改写后:
count.value + _ctx.msg
Vue 把这整个表达式重新拼接成一个"复合表达式(CompoundExpressionNode)",
这样后续生成渲染函数时就能直接插入。
四、为什么不直接用变量名?
假设你模板里写了 {{ count }},
但渲染函数最终运行在一个 JS 环境中,如果没有前缀,它会变成:
kotlin
return count // ❌ JS 会报错:count is not defined
因为这个 count 不是全局变量,而是在组件内部。
Vue 就要帮你加上 _ctx. 或 .value,告诉 JS 去正确的对象上取值。
五、来看看几种常见的改写结果
| 模板写法 | 来源 | 编译结果 |
|---|---|---|
{{ msg }} |
data | _ctx.msg |
{{ title }} |
props | __props.title |
{{ count }} |
setup ref | count.value |
{{ name }} |
setup const | name |
{{ count + msg }} |
混合 | count.value + _ctx.msg |
是不是就清楚多了?
Vue 的目标是让模板看起来简单,但编译后保证代码能正常访问正确的变量。
六、总结一句话
transformExpression的工作就是:找出模板中的每一个变量,
看它属于谁(setup / props / data / 全局),
然后在编译时改写成能正确访问的 JavaScript 代码。
这样你写模板时不用管上下文,Vue 会帮你在编译阶段自动"补全"访问路径。
✅ 小结(一句话复盘)
- 模板 → 编译器扫描表达式
- Babel 解析 → 找出变量
- 判断变量来源 → 加前缀 / 加
.value - 重新拼成新表达式
- 最终生成正确的渲染函数代码
本文部分内容借助 AI 辅助生成,并由作者整理审核。