理解 Vue3 的响应式基础

Q: Vue3 中 ref 和 reactive 有什么区别?什么时候该用哪个?

A: ref 是声明响应式数据的主要 API,可以持有任意类型的值(原始值、对象、数组等),通过 .value 访问内部值;reactive 只能用于对象类型,直接访问属性即可,但存在替换和结构丢失响应式的局限性。最佳实践是尽量使用 ref

使用 ref 创建响应式数据

ref 是 Vue3 中最基础的响应式 API,可以将任意类型的数据包装为响应式引用:

<template>
  <div>{{ name }}</div>
</template>

<script setup>
import { ref } from 'vue';
let name = ref('Bill');
console.log(name);      // RefImpl 对象
console.log(name.value); // 'Bill'

setTimeout(() => {
  name.value = 'Tom'; // 2 秒后自动更新视图
}, 2000);
</script>

要点:

  • ref 返回的是一个响应式对象(RefImpl),需要通过 .value 访问内部的具体值。
  • 模板中会自动解包,不需要写 .value
  • ref 可以持有任意类型:原始值、对象、数组、Map、Set 等。

ref 持有对象

<template>
  <div>{{ bill.name }}</div>
  <div>{{ bill.age }}</div>
</template>

<script setup>
import { ref } from 'vue';
let bill = ref({
  name: 'Bill',
  age: 18
});

setTimeout(() => {
  bill.value.name = 'Bill2';
  bill.value.age = 20;
}, 2000);
</script>

ref 持有数组

<script setup>
import { ref } from 'vue';
let arr = ref([1, 2, 3]);

setTimeout(() => {
  arr.value.push(4, 5, 6); // 触发响应式更新
}, 2000);
</script>

深层响应式

ref 创建的响应式数据默认是深层响应式的。当值是对象且内部有嵌套对象时,嵌套对象的属性变更同样会触发响应式:

<script setup>
import { ref } from 'vue';
let bill = ref({
  name: 'Bill',
  age: 18,
  nested: { count: 1 }
});

setTimeout(() => {
  bill.value.nested.count += 2; // 深层嵌套也会触发更新
}, 2000);
</script>

shallowRef:放弃深层响应式

如果数据结构很大或不需要深层响应,可以使用 shallowRef 来优化性能:

import { shallowRef } from 'vue';

const state = shallowRef({ count: 1 });

// 不会触发响应式更新
state.value.count += 2;

// 只有替换整个 .value 才会触发更新
state.value = { count: 2 };

响应式更新与 nextTick

响应式数据的修改会带来 DOM 的自动更新,但这个更新不是同步的。当修改响应式数据后立即读取 DOM,获取到的是旧值:

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

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

setTimeout(async () => {
  count.value = 2;
  // 此时 DOM 还未更新
  await nextTick();
  // 现在拿到的是最新的 DOM 值
  console.log(container.innerText); // '2'
}, 2000);

onMounted(() => {
  container = document.getElementById('container');
  console.log(container.innerText); // '1'
});
</script>

nextTick 会等待下一次 DOM 更新周期完成后再执行回调,推荐使用 async/await 写法,逻辑更清晰。

使用 reactive 创建响应式数据

reactive 用于将对象转为响应式对象,不需要通过 .value 访问:

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

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

setTimeout(() => {
  state.count1++;
  state.nested.count2 += 2; // 也是深层响应式
}, 2000);
</script>

reactive 也有对应的浅层版本 shallowReactive,不会将嵌套对象转为响应式。

ref 背后的原理

Vue3 的响应式底层是通过 Proxy API 实现的,但 Proxy 只能拦截对象操作,无法拦截原始值。那如果用户想把一个原始值(如字符串、数字)转为响应式怎么办?

两种方案:

  1. 让用户自己处理:用户自己将原始值包装成对象再使用 reactive — 体验不好。
  2. 框架层面处理:多提供一个 API,帮助用户简化操作 — 这就是 ref。

ref 的背后实际上也调用了 reactive:

  • 如果传入的是原始值:内部使用 Object.defineProperty(getter/setter)
  • 如果传入的是对象:内部调用 reactive API

这就是为什么 ref 能够处理任意类型的数据。

reactive 的三大局限性

虽然 ref 和 reactive 都能创建响应式数据,但 reactive 有三个重要的局限性:

1. 值类型受限

reactive 只能用于对象类型(object、array、map、set),不能用于原始值(string、number、boolean)。

2. 不能替换整个对象

let state = reactive({ count: 0 });
// 这会丢失响应式追踪!
state = reactive({ count: 1 });

上面的操作会导致原来的响应式引用丢失,因为 state 被重新赋值为了一个新的代理对象。

3. 对解构操作不友好

let state = reactive({ count: 0 });
let { count } = state; // 解构出来的是普通值,不是响应式的
count++; // 不会触发响应式更新

// 函数传参也同样会丢失响应式
func(state.count); // 传递的是普通值

最佳实践:尽量使用 ref

综合以上分析,推荐使用 ref 作为声明响应式数据的主要 API

特性 ref reactive
支持原始值
支持对象
可替换整个值
支持解构 ✅(配合 toRefs)
模板自动解包
深层响应式 ✅(默认) ✅(默认)

ref 的解包细节

ref 作为 reactive 对象的属性:自动解包

import { ref, reactive } from 'vue';
const name = ref('Bill');
const state = reactive({ name });

console.log(state.name);  // 'Bill'(自动解包,无需 .value)
console.log(name.value);  // 'Bill'

但如果 ref 作为 shallowReactive 对象的属性,则不会自动解包。

常用辅助 API

toRef 与 toRefs

import { reactive, toRef, toRefs } from 'vue';

const state = reactive({ count: 0, message: 'hello' });

// toRef:将响应式对象的某个属性转为 ref
const countRef = toRef(state, 'count');
console.log(countRef.value); // 0

// toRefs:将整个响应式对象的所有属性转为 ref
const stateRefs = toRefs(state);
console.log(stateRefs.count.value);   // 0
console.log(stateRefs.message.value); // 'hello'

unref 与 toValue

import { ref, unref, toValue } from 'vue';

const countRef = ref(10);

unref(countRef);    // 10(语法糖:isRef(val) ? val.value : val)
unref(20);          // 20

// toValue 是 3.3 新增的,支持 getter 函数
toValue(() => 30);  // 30

readonly:创建只读代理

import { ref, readonly } from 'vue';

const count = ref(0);
const countReadonly = readonly(count);
countReadonly.value++; // 会给出警告,不能修改

判断类 API

isRef(refVal);       // 判断是否为 ref 对象
isReactive(state);   // 判断是否为 reactive 代理
isProxy(obj);        // 判断是否由 reactive/readonly 创建的代理
isReadonly(obj);     // 判断是否为只读代理

总结

Vue3 响应式系统的核心是 refreactive。理解它们的工作原理和适用场景,是写好 Vue3 代码的基础:

  1. ref 是主要 API,支持任意类型,通过 .value 访问,模板自动解包。
  2. reactive 仅支持对象类型,不能替换整体,对解构不友好。
  3. 搭配 toRef、toRefs、readonly 等辅助 API,可以灵活处理各种响应式场景。
  4. 牢记 nextTick,当需要在数据变更后获取最新 DOM 时使用。