在 Vue 3 的 Composition API 中,refreactive 是创建响应式状态的两个核心函数。它们都旨在将普通 JavaScript 数据转换为响应式数据,以便在数据变化时自动触发视图更新。然而,它们在处理数据类型、访问方式和底层机制上存在显著差异。理解这些差异对于有效地使用 Composition API 至关重要。

核心思想:ref 用于处理原始值对象,通过 .value 访问其内部值,而 reactive 专门用于处理对象,直接访问对象的属性,且底层基于 Proxy 实现。


一、ref:处理原始值和对象

ref 函数接受一个内部值(inner value),并返回一个响应式的 ref 对象。这个 ref 对象只有一个 value 属性,用来指向内部值。

1.1 定义和用法

  • 定义ref 可以接收任何类型的值作为参数:原始值 (string, number, boolean, null, undefined, Symbol) 或对象 (Object, Array)。
  • 访问:在 JavaScript 中访问 ref 对象时,需要通过其 .value 属性来获取或修改其内部值。在 Vue 的模板中,ref 会自动解包(unwrap),因此可以直接访问,无需 .value

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<template>
<div>
<p>计数器: {{ count }}</p>
<button @click="increment">增加</button>

<p>用户信息: {{ user.name }} - {{ user.age }}</p>
<button @click="changeUserName">修改用户姓名</button>
</div>
</template>

<script setup>
import { ref } from 'vue';

// 1. 处理原始值
const count = ref(0); // count 是一个 ref 对象 { value: 0 }

function increment() {
count.value++; // 在 JS 中访问需要 .value
}

// 2. 处理对象
const user = ref({ name: 'Alice', age: 30 }); // user 也是一个 ref 对象 { value: { name: 'Alice', age: 30 } }

function changeUserName() {
user.value.name = 'Bob'; // 在 JS 中访问对象内部属性时,也需要先 .value 拿到真正的对象
}

// 示例:在模板中自动解包
// <p>{{ count }}</p> 等同于 <p>{{ count.value }}</p>
// <p>{{ user.name }}</p> 等同于 <p>{{ user.value.name }}</p>
</script>

1.2 ref 的特点

  • 内部值可变ref 封装的内部值可以通过 ref.value 随时更改。
  • 模板自动解包:在模板中,如果 ref 对象是顶层属性,Vue 会自动解包其 .value
  • 对象被深度响应式化:如果 ref 接收的是一个对象,Vue 会自动通过 reactive 将其转换为深层响应式对象。这意味着 user.value.name = 'Bob' 这样的操作也能触发更新。
  • unref():可以用来判断一个值是否是 ref 对象,如果是,则返回其内部值,否则返回其自身。
    1
    2
    3
    4
    5
    6
    import { ref, unref } from 'vue';
    const numRef = ref(10);
    const num = 20;

    console.log(unref(numRef)); // 10
    console.log(unref(num)); // 20
  • isRef():判断一个值是否为 ref 对象。
    1
    2
    3
    4
    5
    6
    import { ref, isRef } from 'vue';
    const numRef = ref(10);
    const num = 20;

    console.log(isRef(numRef)); // true
    console.log(isRef(num)); // false
  • toRef() / toRefs():这两个工具函数通常用于将 reactive 对象中的属性转换为 ref,以便解构或传递给子组件时保持响应性。

二、reactive:处理对象

reactive 函数接收一个普通的 JavaScript 对象(或数组),并返回该对象的响应式代理(Proxy)。

2.1 定义和用法

  • 定义reactive 只能接收对象类型 (Object, Array, Map, Set)。如果传入原始值,reactive 会直接返回该原始值,并且不具备响应性。
  • 访问:直接通过对象的属性名访问和修改,就像普通 JavaScript 对象一样,无需 .value

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
<template>
<div>
<p>用户信息: {{ user.name }} - {{ user.age }}</p>
<button @click="changeUserAge">修改用户年龄</button>

<p>商品列表:</p>
<ul>
<li v-for="item in products" :key="item.id">
{{ item.name }} - {{ item.price }}
</li>
</ul>
<button @click="addProduct">添加商品</button>
</div>
</template>

<script setup>
import { reactive } from 'vue';

// 1. 处理对象
const user = reactive({ name: 'Bob', age: 25 }); // user 是一个 Proxy 对象

function changeUserAge() {
user.age++; // 直接访问和修改属性
}

// 2. 处理数组 (数组也是对象)
const products = reactive([
{ id: 1, name: 'Laptop', price: 1200 },
{ id: 2, name: 'Mouse', price: 25 },
]);

function addProduct() {
products.push({ id: products.length + 1, name: 'Keyboard', price: 75 });
}

// 尝试传入原始值 (无效)
const primitiveValue = reactive(100);
console.log(primitiveValue); // 100 (不是响应式对象)
primitiveValue++; // 不会触发任何更新
</script>

2.2 reactive 的特点

  • 深层响应式reactive 会将其对象以及所有嵌套的对象都转换为响应式代理。
  • 基于 Proxyreactive 的实现是基于 ES6 的 Proxy 对象,它能够拦截对对象的各种操作(如属性访问、赋值、删除等)。
  • 解构丢失响应性:对 reactive 对象进行解构 (const { name } = user;) 会导致解构出的变量失去响应性,因为它们不再是 Proxy 对象的属性。
    1
    2
    3
    4
    const state = reactive({ count: 0 });
    let { count } = state;
    count++; // count 只是一个普通数字,state.count 不变
    console.log(state.count); // 0
    为了解决这个问题,可以使用 toRefstoRef
  • isReactive():判断一个值是否为 reactive 对象。
    1
    2
    3
    4
    5
    6
    import { reactive, isReactive } from 'vue';
    const state = reactive({ count: 0 });
    const obj = { count: 0 };

    console.log(isReactive(state)); // true
    console.log(isReactive(obj)); // false
  • markRaw():将一个对象标记为“原始的”,使其永远不会被转换为响应式对象。

三、refreactive 的对比总结

特性 ref reactive
接受类型 原始值和对象 (Object, Array 等) 只能是对象 (Object, Array, Map, Set)
访问方式 在 JS 中通过 .value 访问;在模板中自动解包 直接通过属性名访问;在模板中也直接访问
底层实现 内部值是一个 Proxy 对象 (如果内部值是对象),外部是一个普通对象加上 .value 属性 直接返回一个 Proxy 对象
解构问题 无解构问题,解构 ref 仍需 .valuetoRefs 转换 直接解构会丢失响应性,需要配合 toRefs 使用
深层响应式 如果内部是对象,则自动深层响应式化 默认深层响应式化
用途建议 推荐用于封装原始值,或单个复杂对象(当需要将整个对象替换时) 推荐用于封装多个相关联的属性的对象(如表单数据),或数据集合

四、选择 ref 还是 reactive

选择 ref 还是 reactive 主要取决于你想要封装的数据类型以及你的开发习惯。

  1. 处理原始值 (Primitive Values)

    • 只能使用 refreactive 对原始值无效。
  2. 处理对象 (Objects)

    • 推荐使用 reactive:当你有一个包含多个属性的对象,并且你希望以更自然的方式(无需 .value)访问这些属性时,reactive 是更好的选择。
      1
      2
      3
      4
      5
      6
      const form = reactive({
      username: '',
      password: '',
      rememberMe: false
      });
      // 访问:form.username
    • 特殊情况使用 ref 封装对象
      • 当你需要完全替换整个对象实例时,ref 封装对象会更方便。例如,从后端获取新数据后,需要用新对象替换现有对象:
        1
        2
        3
        4
        5
        6
        const userInfo = ref({ name: 'Old', age: 0 });
        // ...
        async function fetchNewUser() {
        const newUserData = await api.getUser(); // 假设返回 { name: 'New', age: 1 }
        userInfo.value = newUserData; // 替换整个对象
        }
        如果使用 reactive,直接 userInfo = newUserData 会破坏响应性(因为 userInfo 只是一个代理对象,重新赋值会使其指向新的普通对象)。你需要逐个属性赋值:
        1
        2
        3
        4
        5
        6
        const userInfo = reactive({ name: 'Old', age: 0 });
        // ...
        async function fetchNewUser() {
        const newUserData = await api.getUser();
        Object.assign(userInfo, newUserData); // 逐个赋值,保留代理
        }
      • 当你需要将对象作为参数传递给函数或子组件,并希望保持其响应性而不受解构影响时,ref 封装的对象可以避免 reactive 的解构问题(虽然 reactive 可以通过 toRefs 解决)。
  3. 统一风格

    • 全部使用 ref:有些开发者倾向于所有响应式数据都使用 ref,以保持代码风格一致,并且始终通过 .value 访问。这可以避免忘记 .value 的情况。
      1
      2
      const count = ref(0);
      const user = ref({ name: 'Alice', age: 30 }); // 封装对象
    • 混用 refreactive:Vue 官方鼓励根据数据类型合理混用。原始值用 ref,对象用 reactive

总结性建议

  • 默认使用 ref,因为它对原始值和对象都适用,且模板中自动解包,使用起来更“通用”。
  • 当处理结构复杂的对象,且需要通过多个属性来操作时,考虑使用 reactive,因为它提供更自然的 JavaScript 对象操作体验。
  • 理解 reactive 的解构问题,并在需要时使用 toRefs 辅助。

五、实际应用场景示例

5.1 reactive 封装表单数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<template>
<form @submit.prevent="submitForm">
<input type="text" v-model="formData.username" placeholder="用户名" />
<input type="password" v-model="formData.password" placeholder="密码" />
<button type="submit">登录</button>
</form>
<p v-if="loginError">{{ loginError }}</p>
</template>

<script setup>
import { reactive, ref } from 'vue';

const formData = reactive({
username: '',
password: '',
});

const loginError = ref(''); // 错误信息用 ref 封装原始值

function submitForm() {
if (formData.username === 'test' && formData.password === '123') {
alert('登录成功!');
loginError.value = '';
} else {
loginError.value = '用户名或密码错误。';
}
}
</script>

5.2 ref 封装单个状态和通过 toRefs 处理 reactive 解构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<template>
<div>
<h2>User Profile</h2>
<p>Name: {{ nameRef }}</p>
<p>Email: {{ emailRef }}</p>
<button @click="changeProfile">修改信息</button>
</div>
</template>

<script setup>
import { reactive, toRefs } from 'vue';

const userProfile = reactive({
name: 'Charlie',
email: 'charlie@example.com',
age: 28,
});

// 使用 toRefs 将 reactive 对象的属性转换为 ref 集合
// 这样解构出来的 nameRef 和 emailRef 都是 ref 对象,保持了响应性
const { name: nameRef, email: emailRef } = toRefs(userProfile);

function changeProfile() {
nameRef.value = 'David'; // 修改 nameRef.value 会影响 userProfile.name
emailRef.value = 'david@example.com';
// userProfile.age 也仍然是响应式的
userProfile.age = 29;
}
</script>

六、总结

refreactive 都是 Vue 3 Composition API 中强大的响应式工具。ref 提供了一个通用的封装,能够处理所有类型的数据,通过 .value 访问,并在模板中自动解包。reactive 则专为对象设计,提供更直接的属性访问体验,但需注意解构带来的响应性丢失问题,并可配合 toRefs 解决。在实际开发中,开发者可以根据具体的数据类型、操作习惯以及是否需要替换整个对象等因素,灵活选择使用它们,或者将它们结合起来使用。理解它们的差异和适用场景,有助于写出更高效、更易维护的 Vue 3 应用。