Vue 3 响应式原理:computed的模板解包机制

1 阅读3分钟

前言

vue中的计算属性相关知识:

核心概念对比

特性脚本环境(JS)模板环境(Template)
ref 访问.value 必需自动解包(无需 .value
computed 访问.value 必需自动解包(无需 .value
原始对象访问直接访问直接访问

二话不说先上代码,大家觉得会输出什么呢?

<template>
  <div>{{ firstName }}+{{lastName}}</div>
  <div>{{ name }}</div>
  <div>{{ name.value }}</div>
</template>

<script setup>
import { ref, computed } from "vue";
const firstName = ref("ac");
const lastName = ref("bc");

console.log(firstName);

const name = computed(() => {
  return firstName.value + lastName.value;
});
console.log(name);
console.log(name.value);
</script>

<style>
</style>

结果如下所示,大家可能有点疑惑?接下来就看下面知识点:

image-20250327204715671.png

const firstName = ref("ac");   
// 创建响应式引用,value = "ac"
const lastName = ref("bc");    
// 创建响应式引用,value = "bc"

控制台首次输出

console.log(firstName);  // 输出 RefImpl 对象
console.log(name);       // 输出 ComputedRefImpl 对象
console.log(name.value); // 输出计算值 "acbc"

模板渲染机制

模板编译结果对比

模板写法实际渲染值原理说明
{{ firstName }}"ac"自动解包 ref,等价于 firstName.value
{{ name }}"acbc"自动解包 computed,等价于 name.value
{{ name.value }}空/undefined错误用法,因为 name 解包后得到字符串,字符串没有 .value 属性

为什么表现不同?

  1. Ref 对象结构

    interface Ref<T> {
      value: T
      __v_isRef: true
      // ...其他内部属性
    }
    
  2. Computed 对象结构

    interface ComputedRef<T> extends Ref<T> {
      readonly effect: ReactiveEffect<T>
      // ...其他计算属性特有属性
    }
    
  3. 模板自动解包规则

    • 遇到 {{ variable }} 时:
      function renderValue(target) {
        return isRef(target) ? target.value : target
      }
      

控制台输出详解

1. console.log(firstName)

输出示例:

RefImpl {
  __v_isRef: true,
  _value: "ac",
  value: "ac"  // 通过 getter 访问
}

2. console.log(name)

输出示例:

ComputedRefImpl {
  __v_isRef: true,
  _dirty: false,
  _value: "acbc",  // 缓存的计算结果
  effect: ReactiveEffect {...}
}

3. console.log(name.value)

输出结果:

"acbc"  // 直接获取计算值

正确用法总结

脚本区域(<script setup>

// 必须使用 .value 访问
const fullName = firstName.value + lastName.value
console.log(name.value) // 正确

模板区域(<template>

<!-- 直接使用变量名 -->
<div>{{ name }}</div>  

<!-- 等价于 -->
<div>{{ name.value }}</div>  <!-- 错误! -->

常见误区解释

  1. 为什么模板不能写 .value

    • Vue 的模板编译器已经自动处理解包
    • name.value 相当于尝试访问字符串的 .value 属性
  2. computed 和普通 ref 的区别

    特性常规 refcomputed
    值获取直接赋值通过计算函数获取
    依赖追踪自动追踪
    模板使用自动解包自动解包
  3. 调试建议

    <!-- 调试 computed 对象 -->
    <pre>{{ { computedObject: name } }}</pre>
    

说人话:

1. refcomputed 的本质

  • 它们都是 Vue 的**"魔法盒子"**,里面装着值(比如字符串、数字等)
  • 但你要从盒子里拿值,在 JS 里必须用 .value 打开盒子
  • 在模板里,Vue 会自动帮你打开盒子,你直接拿就行

2. 你的代码问题

<div>{{ name.value }}</div>  <!-- 错误! -->

相当于:

  1. Vue 先自动打开 name 盒子 → 得到字符串 "acbc"
  2. 然后你让 Vue 去拿 "acbc".value → 字符串哪有 .value 啊!(报错)

3. 正确姿势对比

场景JS 代码里模板里原因
普通 reffirstName.value{{ firstName }}模板自动拆盒子
computedname.value{{ name }}模板自动拆盒子
原始值直接 "ac"{{ "ac" }}本来就不是盒子

4. 通俗比喻

  • JS 环境:就像你在厨房做菜(需要自己动手开冰箱拿食材)
    // 必须自己开盒子!
    const food = fridge.value 
    
  • 模板环境:就像你点外卖(商家自动帮你打包好)
    <!-- 直接吃现成的 -->
    <div>{{ food }}</div>  
    

5. 记住三句话

  1. <script setup> 里:必须写 .value(手动开盒)
  2. <template> 里:永远不要写 .value(自动拆盒)
  3. computedref 在模板里待遇一样,都自动拆

这样写就对了:

<template>
  <div>{{ name }}</div>  <!-- 显示 "acbc" -->
</template>

<script setup>
const name = computed(() => "ac" + "bc")
console.log(name.value) // 这里必须写 .value
</script>