响应式
约 1069 字大约 4 分钟
2026-01-02
所谓响应式,指的是数据变化到依赖该数据的视图、计算、监听自动进行更新。其核心在于数据驱动视图,避免手动操作 DOM
ref
ref 用于声明任意类型的响应式状态,返回一个 Ref<T>。在非模板中访问或修改时需要通过 .value
<script setup>
import { ref } from 'vue';
const name = ref('Bill');
setTimeout(() => {
name.value = 'Tim';
}, 2000);
</script><script setup>
import { ref } from 'vue';
const user = ref({
name: 'ZhaoJisen',
profile: { age: 20 },
});
const grow = () => {
user.value.profile.age++;
};
</script>
<template>
<div>{{ user.name }}</div>
<div>{{ user.profile.age }}</div>
<button @click="grow">grow</button>
</template>当 ref 包裹一个对象时会逐层将对象的每一层级赋予响应式
自动解包
模板内使用 ref 时会自动解包,无需 .value
<template>
<span>{{ count }}</span>
</template>
<script setup>
import { ref } from 'vue';
const count = ref(0);
</script>顶层绑定
仅当 ref 是 顶层绑定 时模板才会自动解包,把 ref 嵌在普通对象里会将会失去解包能力。解决方式就是把对象变成响应式,或显式使用 .value
// {{ object.foo + 1 }} => [object Object]1
const object = { foo: ref(1) };嵌套reactive
当 ref 嵌套在 reactive 对象中访问/修改时会自动解包。但是依然存在例外情况,当 ref 位于响应式数组或 Map/Set 等集合中时,不会解包,需要手动 .value
const books = reactive([ref('Vue 3 Guide')]);
console.log(books[0].value);
const map = reactive(new Map([['count', ref(0)]]));
console.log(map.get('count').value);reactive
reactive 更适合数组/对象/Map/Set 等复合结构,返回一个 Proxy。代理对象与源对象不相等,但对同一对象会保持代理稳定性。
import { reactive } from 'vue';
const state = reactive({
nested: { count: 0 },
list: ['foo', 'bar'],
});
const mutateDeeply = () => {
state.nested.count++;
state.list.push('baz');
};提示
对同一个原始对象多次调用 reactive 会返回同一个代理。
局限与常见坑
注意
- 仅对对象类型生效(对象、数组、Map、Set 等),对原始类型无效
- 必须保持同一引用,随意替换会丢失原有响应性连接
- 解构会丢失响应性,使用
toRef/toRefs保留连接 - 传参时不要传
state.count这样的裸值;需要传 ref 或整个 state :::
深层与浅层响应式
Vue 默认是深层响应式;需要限制深度时使用 shallowRef / shallowReactive,避免深层追踪带来的成本。
nextTick
响应式数据的更新,带来了 DOM 的自动更新,但 DOM 更新是批量异步的:状态变化会被缓存在队列里,在下一次 “tick” 统一刷新。nextTick() 允许你在 DOM 完成更新后读取真实状态。
<script setup>
import { reactive, ref, nextTick } from 'vue';
const titleRef = (ref < HTMLElement) | (null > null);
const state = reactive({ title: 'This is Title' });
const setTitle = async () => {
state.title = '这是标题';
await nextTick();
console.log(titleRef.value?.innerText); // 这是标题
};
</script>
<template>
<div>
<h1 ref="titleRef">{{ state.title }}</h1>
<button @click="setTitle">change Title</button>
</div>
</template>相关信息
在 Options API 中使用 this.$nextTick(),回调里的 this 会被绑定到组件实例。
Options API 的 data
data 必须是函数,返回对象会被 Vue 转成响应式并代理到组件实例上。
export default {
data() {
return { message: 'Hello Vue' };
},
};this.$data 指向原始响应式对象,this.message 只是代理。直接在实例上新增属性不会变成响应式数据,应该在 data() 中声明。
提示
Vue2 中 data 允许是对象,但 Vue3 中必须是函数。
Vue2 与 Vue3 的实现差异
Vue3 使用 Proxy 拦截 get/set/has/ownKeys 等操作,天然支持新增/删除属性与数组索引更新。Vue2 通过 Object.defineProperty 递归劫持属性,无法直接侦测新增属性或索引变更(需要 Vue.set)。
赋值场景的差异示例:
export default {
data() {
return { someObject: {} };
},
mounted() {
const newObject = {};
this.someObject = newObject;
console.log(this.someObject === newObject); // Vue3: false, Vue2: true
},
};在 shallowReactive 中不会自动解包
reactive
Vue 响应式底层是通过 Proxy API 来实现的,但 Proxy API 只能对对象进行拦截,无法对原始值进行拦截
ref 背后也调用了 reactive api,对于 原始值 ref 调用了 Object.defineProperty 对于复杂值 ref 调用了 reactive
shadowReactive
最佳实现为:尽量使用 ref 来声明响应式数据的主要 api
reactive 具有局限性
- 值类型有限,只能是对象类型
- 不能替换响应式对象,否则丢失响应式追踪
- 解构丢失响应式