Vue 模板中 {{ double }} 自动解包为 double.value,是编译时静态转换;setup 中仍需 double.value;provide/inject 通过运行时 unref 解包;reactive 嵌套、数组索引、普通解构等场景不解包。

在 Vue 模板中直接写 {{ double }} 而不用 {{ double.value }},不是省略或语法糖,而是 Vue 在编译阶段就识别出这是 computed 返回的 ComputedRef,并自动做了值解包——你看到的就是它实时计算出的值,不是那个带 .value 的包装对象。
模板里 computed 自动解包,和 ref 一样有编译支持
Vue 编译器会扫描所有插值({{ }})、指令(如 :class、v-if、v-model)中的变量。只要这个变量是 computed() 创建的响应式引用(即 ComputedRef 类型),就会在生成的渲染函数中插入 .value 访问逻辑,等价于帮你写了 ctx.double.value。
这个过程发生在编译时,不走运行时 Proxy 拦截,没有性能损耗,也不影响依赖追踪。
-
{{ double }}→ 自动转为double.value -
:title="double"→ 自动转为double.value -
v-show="double"→ 自动转为double.value(布尔判断用的是解包后的值) -
v-model="double"→ 仅当绑定到支持双向绑定的表单元素,且double是可写的computed(有set)才生效;此时读取和赋值都由 Vue 自动处理.value
setup 函数内部仍需 .value,因为不在模板上下文
computed 返回的始终是一个 Ref 对象(准确说是 ComputedRef<t></t>),它的本质就是个带 .value getter/setter 的响应式容器。
所以在 JS 逻辑中——比如 setup()、watch()、onMounted() 或普通函数里——你必须显式访问 .value 才能拿到当前值:
const count = ref(1) const double = computed(() => count.value * 2) console.log(double) // ComputedRef<number> 对象 console.log(double.value) // ✅ 正确:拿到数字 2
如果不加 .value,你拿到的是整个计算引用,不是计算结果,容易导致类型错误或逻辑失效。
provide/inject 中也自动解包,但机制不同
当你用 provide('msg', computed(() => this.message)),子组件通过 inject('msg') 获取时,Vue 在初始化组件时会对注入值做一次 unref() 处理(类似 isRef(x) ? x.value : x)。
所以子组件中 this.msg 直接是字符串 'hello',而不是 Ref<string></string>,自然也不需要 .value。
这和模板解包是两套机制:一个是编译期静态转换,一个是运行时注入阶段的自动扁平化。
不会自动解包的典型场景
这些地方 Vue 无法或不会帮你拆包,必须手动处理:
- 当
computed作为对象属性嵌套使用:{{ state.double }}(其中state是reactive({ double }))→ ❌ 不解包,要写{{ state.double.value }} - 放在数组里再取:
{{ list[0] }}(若list[0]是computed(...))→ ❌ 显示整个ComputedRef对象 - 解构后失去响应式标识:
const { double } = toRefs({ double: computed(...) }) // ✅ 这种 toRefs 包装后的解构,模板中 {{ double }} 仍可解包 const obj = { double: computed(...) } const { double } = obj // ❌ 普通 JS 解构,无响应式标记,模板中不解包也不响应
不复杂但容易忽略

















