Appearance
✨ 响应式基础 👌
本章目标:
- 掌握
ref与reactive的使用场景与差异- 理解
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); // 需要 .valuevue
<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;浅响应只在明确需要控制更新粒度时使用。
