Skip to content

✨ 响应式基础 👌

本章目标:

  • 掌握 refreactive 的使用场景与差异
  • 理解 shallowRef / shallowReactive 的行为
  • 清楚 DOM 更新时机与 nextTick
  • 熟悉模板中的自动解包规则与常见陷阱

使用 ref

ref 用于创建一个可响应的“值容器”。它返回一个对象,需要通过 .value 读写内部值。模板中会自动解包 ref,因此可以直接使用:

vue
<template>
  <div>{{ name }}</div>
  <!-- 模板自动解包,无需 .value -->
</template>

<script setup>
import { ref } from "vue";
const name = ref("Bill");

console.log(name); // { value: 'Bill' }
console.log(name.value); // 'Bill'

setTimeout(() => {
  name.value = "Tom";
}, 2000);
</script>

ref 可以持有任意类型:原始值、对象、数组、甚至 Map/Set

对象值(深层响应)的示例:

vue
<template>
  <div>{{ person.name }}</div>
  <div>{{ person.age }}</div>
  <div>{{ person.nested.count }}</div>
  <!-- 模板中的对象属性同样会被自动解包 -->
  <!-- 注意:仅在模板内自动解包,脚本内仍需 .value -->
</template>

<script setup>
import { ref } from "vue";
const person = ref({
  name: "Bill",
  age: 18,
  nested: { count: 1 },
});

setTimeout(() => {
  person.value.name = "Bill2";
  person.value.age = 20;
  person.value.nested.count += 2;
}, 2000);
</script>

数组值的示例:

vue
<template>
  <div>{{ arr }}</div>
</template>

<script setup>
import { ref } from "vue";
const arr = ref([1, 2, 3]);
setTimeout(() => {
  arr.value.push(4, 5, 6);
}, 2000);
</script>

深浅响应:shallowRef

shallowRef 只对 .value 的替换做响应,内部对象不会被深度转为响应式:

js
import { shallowRef } from "vue";

const state = shallowRef({ count: 1 });
// 不触发更新(仅内部字段变化)
state.value.count += 2;
// 触发更新(替换 .value)
state.value = { count: 2 };

完整示例:

vue
<template>
  <div>{{ person.name }}</div>
  <div>{{ person.age }}</div>
  <div>{{ person.nested.count }}</div>
</template>

<script setup>
import { shallowRef } from "vue";
const person = shallowRef({
  name: "Bill",
  age: 18,
  nested: { count: 1 },
});

// 不会触发视图更新
setTimeout(() => {
  person.value.name = "Bill2";
  person.value.age = 20;
  person.value.nested.count += 2;
}, 2000);

// 会触发视图更新(替换引用)
setTimeout(() => {
  person.value = {
    name: "Bill3",
    age: 30,
    nested: { count: 3 },
  };
}, 4000);
</script>

DOM 更新与 nextTick

响应式状态改变会安排一次异步 DOM 更新。修改后立即读取 DOM,拿到的是“旧值”。使用 nextTick 等待 DOM 更新完成:

vue
<template>
  <div id="container">{{ count }}</div>
</template>

<script setup>
import { ref, onMounted, nextTick } from "vue";
const count = ref(1);
let container = null;

onMounted(() => {
  container = document.getElementById("container");
  console.log("第一次打印:", container.innerText);
});

setTimeout(async () => {
  count.value = 2; // 修改响应式状态
  await nextTick(); // 等到 DOM 更新完成
  console.log("第二次打印:", container.innerText); // 最新值
}, 2000);
</script>

不使用 async/await 也可以用回调形式:

js
nextTick(() => {
  // 此处 DOM 已更新
});

使用 reactive

reactive 将一个对象转为响应式对象,适用于包含多个字段的状态:

vue
<template>
  <div>{{ state.count1 }}</div>
  <div>{{ state.nested.count2 }}</div>
  <!-- 模板中使用无需 .value,因为 reactive 返回的就是“响应式对象” -->
</template>

<script setup>
import { reactive } from "vue";
const state = reactive({
  count1: 0,
  nested: { count2: 0 },
});

setTimeout(() => {
  state.count1++;
  state.nested.count2 += 2;
}, 2000);
</script>

说明:Vue 3 的响应式对对象使用 Proxy 拦截;原始值无法用 Proxy 直接拦截,因此使用 ref.value getter/setter 来进行依赖收集和触发(比如Object.defineProperty)。ref 在持有对象值时,会将对象深度转为响应式(等价于 reactive)。

浅响应对象可使用 shallowReactive

vue
<template>
  <div>{{ state.count1 }}</div>
  <div>{{ state.nested.count2 }}</div>
</template>

<script setup>
import { shallowReactive } from "vue";
const state = shallowReactive({
  count1: 0,
  nested: { count2: 0 },
});

// 仅顶层字段变化会被追踪
setTimeout(() => {
  state.count1++;
}, 2000);

// 嵌套对象字段变化不会触发更新
setTimeout(() => {
  state.nested.count2++;
}, 4000);
</script>

使用细节与最佳实践

  • 优先使用 ref 管理状态(类型推断好、迁移灵活、原始值直接可用)。
  • 当状态天然是“一个对象”、且需要按对象语义使用时使用 reactive(如表单模型)。
  • 避免替换 reactive 对象本身的引用,否则会丢失响应式:
js
let state = reactive({ count: 0 });
// ❌ 替换引用会导致旧对象不再被追踪
state = reactive({ count: 1 });
  • 注意解构会丢失响应式:
js
const state = reactive({ count: 0 });
let { count } = state; // 解构得到的是普通值
count++; // 不会触发响应式更新

// 函数传参同理:传入的是普通值
func(state.count);

::: important 响应式依赖于 Proxy 对属性访问的拦截,解构赋值创建了新的变量,这些变量不再被原始对象的 Proxy 所管理,因此失去了响应式能力。使用 toRefs() 或者 toRef()可以保持这种连接。 :::


ref 解包规则与常见陷阱

1) 作为 reactive 对象的属性(自动解包)

vue
<script setup>
import { ref, reactive } from "vue";
const name = ref("Bill");
const state = reactive({ name });

console.log(state.name); // 自动解包(Bill)
console.log(name.value); // Bill
</script>

2) 作为 shallowReactive 的属性(不解包)

vue
<script setup>
import { ref, shallowReactive } from "vue";
const name = ref("Bill");
const state = shallowReactive({ name });

console.log(state.name.value); // 不会自动解包
</script>

变化仍会被追踪(因为属性本身是个 ref):

vue
<template>
  <div>{{ state.name.value }}</div>
</template>

<script setup>
import { ref, shallowReactive } from "vue";
const name = ref("Bill");
const state = shallowReactive({ name });
setTimeout(() => {
  name.value = "Tom";
}, 2000);
</script>

3) 练习:失去关联的问题

vue
<template>
  <div>{{ obj.name }}</div>
</template>

<script setup>
import { ref, shallowReactive } from "vue";
const name = ref("Bill");
const obj = shallowReactive({ name });

setTimeout(() => {
  obj.name = "John"; // 将 ref 属性替换为普通字符串,失去关联
}, 1000);
setTimeout(() => {
  name.value = "Smith"; // 不再影响 obj.name
}, 2000);
</script>

答案:

  • shallowReactive 内的 ref 不自动解包,因此渲染会带引号显示对象的字符串化结果;
  • 1 秒后 obj.name 被替换为普通字符串,从原来的 ref 失去关联,后续 name.value 改变不会影响渲染。

正确写法(保持关联):

js
obj.name.value = "John";
// 后续修改 name.value 仍会生效

另一个示例(切换为另一个 ref):

vue
<template>
  <div>{{ obj.name.value }}</div>
</template>

<script setup>
import { ref, shallowReactive } from "vue";
const name = ref("Bill");
const stuName = ref("John");
const obj = shallowReactive({ name });

// 切换为另一个 ref,保持响应式但与原 ref 失去关联
obj.name = stuName;

setTimeout(() => {
  name.value = "Tom"; // 不影响 obj.name
}, 2000);
setTimeout(() => {
  stuName.value = "Smith"; // 会影响 obj.name
}, 4000);
</script>

4) 数组与 Map 中的 ref(不解包)

ref 位于响应式数组或集合中时,不会自动解包:

js
import { ref, reactive } from "vue";

const books = reactive([ref("Vue 3 Guide")]);
console.log(books[0].value); // 需要 .value

const map = reactive(new Map([["count", ref(0)]]));
console.log(map.get("count").value); // 需要 .value
vue
<script setup>
import { ref, reactive } from "vue";
const name = ref("Bill");
const score = ref(100);
const state = reactive({
  name, // 属性上的 ref(自动解包)
  scores: [score], // 数组中的 ref(不解包)
});
console.log(state.name); // Bill(自动解包)
console.log(state.scores[0]); // Ref 对象
console.log(state.scores[0].value); // 100
</script>

5) 模板中的自动解包(仅顶级)

模板中仅“顶级” ref 会自动解包:

vue
<template>
  <div>
    <div>{{ count }}</div>
    <!-- 顶级 ref 自动解包 -->
    <div>{{ object.id }}</div>
    <!-- 非顶级 ref 表面看能渲染,但不是解包 -->
  </div>
</template>

<script setup>
import { ref } from "vue";
const count = ref(0);
const object = { id: ref(1) }; // 非顶级 ref
</script>

验证:

vue
<template>
  <div>
    <div>{{ count + 1 }}</div>
    <!-- 1 -->
    <div>{{ object.id + 1 }}</div>
    <!-- [object Object]1 -->
    <div>{{ object.id.value + 1 }}</div>
    <!-- 2 -->
  </div>
</template>

小结

  • ref 适合所有类型的状态,模板自动解包,脚本需 .value
  • reactive 适合对象状态,返回的就是响应式对象,直接读写字段即可。
  • 使用浅响应(shallowRef/shallowReactive)时,只有替换引用会触发更新;内层字段变更不会触发更新。
  • DOM 更新是异步的,用 nextTick 获取更新后的 DOM。
  • 常见陷阱:替换响应式对象引用、解构导致丢失响应式、数组/集合中的 ref 不自动解包、模板仅顶级 ref 自动解包。

建议:优先使用 ref 作为状态首选;需要对象语义时使用 reactive;浅响应只在明确需要控制更新粒度时使用。