滴水恩情 发表于 2025-4-18 18:03:58

Vue 3 reactive 和 ref 区别及 失去响应性问题

在 Vue 3 中,reactive 和 ref 是实现响应式数据的两个核心 API,它们的设计目标和使用场景有所差别。以下是两者的详细对比:
1. 基本定义与核心功能

特性reactiveref作用创建对象范例的响应式署理(对象、数组、Map 等)创建一个响应式的引用,可用于基本范例或对象返回值原始对象的署理(Proxy)包含 value 属性的响应式对象(如 { value: xxx })响应式原理基于对象的属性劫持(Proxy + Reflect)基于 getter/setter 劫持,基本范例和对象统一处理解包机制无(直接访问属性)对象会主动解包为 reactive,基本范例需通过 .value 访问 2. 详细区别

(1) 实用数据范例



[*] reactive
仅用于对象范例(包罗普通对象、数组、Map、Set 等),无法直接处理基本范例(如 string、number、boolean)。
javascript
// 正确:对象类型
const state = reactive({ count: 0 });
// 错误:不能直接处理基本类型(会被视为对象属性名)
const num = reactive(1); // 报错:reactive() expects an object

[*] ref
可处理所有范例(基本范例或对象)。对于对象,ref 内部会主动通过 reactive 转为响应式署理(即 “主动解包”)。
javascript
// 基本类型
const count = ref(0);
// 对象类型(内部自动转为 reactive 代理)
const obj = ref({ name: 'Vue' });

(2) 访问方式



[*] reactive
直接通过属性访问,无需额外语法:
javascript
const state = reactive({ count: 0 });
state.count = 1; // 直接修改属性

[*] ref

[*]基本范例:需通过 .value 访问和修改: javascript
const count = ref(0);
count.value = 1; // 修改需通过 .value

[*]对象范例:由于主动解包,可直接访问属性(内部已转为 reactive 署理): javascript
const obj = ref({ name: 'Vue' });
obj.value.name = 'Vue 3'; // 直接修改属性(等价于 reactive 的操作)
// 或通过解包后访问(模板中无需 .value)


(3) 响应式解包规则



[*] 在模板中

[*]reactive 创建的对象:直接通过属性名访问,无需特殊处理。
[*]ref 创建的值:

[*]基本范例:模板中会主动解包,直接使用 {{ count }} 即可(无需 {{ count.value }})。
[*]对象范例:同样主动解包,等价于 reactive,直接访问属性(如 {{ obj.name }})。


[*] 在 JavaScript 中

[*]若从 ref 中解构属性,需使用 toRefs 或手动保留 ref 引用,否则会失去响应式: javascript
const obj = ref({ name: 'Vue', age: 3 });
const { name } = obj.value; // 错误:name 是普通值,失去响应式
const { name } = toRefs(obj.value); // 正确:通过 toRefs 保持响应式


(4) 重新赋值与响应式



[*] reactive
署理对象指向固定,不能直接重新赋值为新对象(否则会失去响应式),需通过修改属性实现更新:
javascript
const state = reactive({ count: 0 });
state = { count: 1 }; // 错误:直接赋值会导致响应式丢失
state.count = 1; // 正确:修改属性

[*] ref
可以重新赋值为新值(基本范例或对象),响应式会主动更新:
javascript
const count = ref(0);
count.value = 1; // 基本类型重新赋值(正确)

const obj = ref({ name: 'Vue' });
obj.value = { name: 'React' }; // 对象重新赋值(正确,内部会重新创建 reactive 代理)

(5) 组合式 API 中的使用



[*] reactive
适合定义包含多个属性的对象状态,常用于复杂状态管理:
javascript
setup() {
const state = reactive({
    count: 0,
    user: { name: 'Alice' }
});
return { state }; // 模板中通过 state.count 访问
}

[*] ref
适合定义单个值(基本范例或对象),或须要在函数间转达的独立状态,且返回时无需嵌套对象:
javascript
setup() {
const count = ref(0);
const user = ref({ name: 'Alice' });
return { count, user }; // 模板中直接使用 count 和 user.name
}

3. 典型使用场景

场景reactive 更合适ref 更合适定义对象 / 数组状态✅(reactive({ a: 1, b: 2 }))✅(ref({ a: 1, b: 2 }),主动解包)定义基本范例状态(如计数)❌(不支持)✅(ref(0))函数返回单个响应式值❌(需返回对象)✅(直接返回 ref(0),模板主动解包)解构响应式对象并保持响应式须要共同 toRefs(const { a, b } = toRefs(state))直接解构 ref(如 const { value } = count,但通常无需解构)动态创建响应式变量需手动构建对象直接使用 ref 包裹恣意值 4. 最佳实践


[*] 基本范例用 ref:
无论何时须要响应式的基本范例(如 count、isLoading),直接使用 ref。
javascript
const count = ref(0);
const isLoading = ref(false);

[*] 对象 / 数组用 ref 或 reactive:

[*]若状态是单个对象 / 数组,推荐用 ref(统一接口,主动解包): javascript
const user = ref({ name: 'Vue', age: 3 });

[*]若须要在一个对象中整合多个状态(如表单数据),可用 reactive: javascript
const form = reactive({
name: 'Alice',
email: 'alice@example.com'
});


[*] 解构时保持响应式:
当从 reactive 对象中解构属性时,必须使用 toRefs 制止失去响应式:
javascript
const state = reactive({ a: 0, b: 0 });
const { a, b } = toRefs(state); // a 和 b 是 ref,保持响应式

[*] 制止混合使用导致杂乱:

[*]尽量统一风格:若状态是对象,要么全用 reactive,要么全用 ref 包裹对象。
[*]优先使用 ref 包裹对象:因为 ref 可以无缝处理基本范例和对象,且返回值更简便(无需嵌套对象)。

总结



[*]ref 是更通用的选择:可以处理所有数据范例,对象会主动解包为 reactive,且在组合式 API 中返回更方便(直接返回独立的 ref 变量)。
[*]reactive 适合复杂对象:当须要定义包含多个属性的对象状态,且不想通过 .value 访问时(固然 ref 包裹对象后也无需 .value,但 reactive 更直接)。

明白两者的核心区别后,可根据具体场景选择最合适的 API,制止响应式失效问题(如之前代码中 reactive 对象直接赋值导致的更新失败,改用 ref 或正确更新属性即可解决)。

====================================================================
当须要整体更新 reactive 对象中的某个嵌套对象(例如从接口获取全新的用户数据并覆盖原有对象),同时又要保持响应式时,关键是不替换整个 reactive 对象的引用,而是通过以下两种方式更新其属性:
方法一:使用 Object.assign 合并新对象(推荐)

实用场景:



[*]已有 reactive 对象(如 state.user 是 reactive 创建的署理对象)。
[*]须要用新对象的属性覆盖或补充原有对象的属性,而非完全替换整个对象的引用。
示例:

javascript
import { reactive } from 'vue';

// 初始化:state 是 reactive 对象,user 是嵌套对象
const state = reactive({
user: { name: 'Alice', age: 30 }
});

// 假设从接口获取新用户数据(普通对象)
const newUser = { name: 'Bob', age: 35, city: 'Beijing' };

// 正确写法:将新属性合并到现有 reactive 对象中(保持代理引用)
Object.assign(state.user, newUser);
// 等价于:state.user = { ...state.user, ...newUser }; (但此写法错误,见下方说明)
关键原理:



[*]Object.assign(target, source) 会将 source 的属性直接复制到 target(即 state.user 署理对象),不改变 state.user 的引用,因此响应式得以保留。
[*]错误写法:state.user = { ...state.user, ...newUser } 会创建一个新的普通对象并赋值给 state.user,导致其失去 reactive 署理,响应式失效。
方法二:重新赋值时保持 reactive 署理(实用于全新对象)

实用场景:



[*]须要完全替换 reactive 对象中的某个嵌套对象(例如 state.user 本来不存在,或须要用全新的对象结构覆盖)。
[*]确保新对象被重新包裹为 reactive 署理,或通过 ref 间接处理(更推荐)。
方式 1:对新对象重新应用 reactive(不推荐,可能导致性能问题)

javascript
// 错误:直接赋值新普通对象(失去响应式)
state.user = newUser;

// 正确:对新对象创建 reactive 代理后再赋值(保持响应式)
state.user = reactive(newUser);
// 注意:此时 state.user 是新的代理对象,原有代理会被丢弃,可能导致不必要的依赖追踪
方式 2:用 ref 包裹对象(更优雅的解决方案)

假如嵌套对象须要频繁整体更新,发起将其定义为 ref,而非直接作为 reactive 的属性:

javascript
import { reactive, ref } from 'vue';

// 初始化:用 ref 包裹 user 对象(可整体更新)
const state = reactive({
user: ref({ name: 'Alice', age: 30 }) // user 是 ref 对象
});

// 从接口获取新用户数据
const newUser = { name: 'Bob', age: 35, city: 'Beijing' };

// 正确写法:直接赋值给 ref 的 value(内部自动处理响应式)
state.user.value = newUser;
// 或先解包再赋值(模板中无需 .value,自动解包)
原理:



[*]ref 答应重新赋值 value(包罗对象),且会主动对新对象创建 reactive 署理(即 “主动解包”),因此响应式得以保留。
[*]相比直接使用 reactive 嵌套对象,ref 更机动,适合须要整体替换的场景。
方法三:解构新对象并逐个更新属性(实用于少量属性)

实用场景:



[*]新对象与原对象结构差别较小,只需更新部门属性。
[*]希望保持代码可读性,制止使用 Object.assign。
示例:

javascript
// 假设 newUser 包含需要更新的属性
state.user.name = newUser.name;
state.user.age = newUser.age;
state.user.city = newUser.city; // 直接更新每个属性,保持代理引用
核心原则:制止丢失署理引用

无论接纳哪种方法,核心是确保 reactive 对象的引用不被替换:


[*]对 reactive 嵌套对象:通过修改其属性或合并新属性(Object.assign),而非重新赋值整个对象。
[*]对频繁整体更新的对象:改用 ref 包裹(const obj = ref(initialValue)),答应直接赋值新对象到 obj.value,利用 ref 的主动响应式处理。
完整示例对比(错误 vs 正确)

错误写法(失去响应式):

javascript
const state = reactive({ user: { name: 'Alice' } });
// 错误:直接赋值新对象,user 失去 reactive 代理
state.user = { name: 'Bob' };
// 视图不会更新,因为 state.user 现在是普通对象
正确写法 1(合并属性):

javascript
const state = reactive({ user: { name: 'Alice' } });
const newUser = { name: 'Bob', age: 30 };
// 正确:合并新属性到现有代理对象
Object.assign(state.user, newUser);
// 或逐个赋值:state.user.name = 'Bob'; state.user.age = 30;
// 视图会正确更新
正确写法 2(用 ref 包裹对象):

javascript
const state = reactive({ user: ref({ name: 'Alice' }) });
const newUser = { name: 'Bob', age: 30 };
// 正确:赋值给 ref 的 value,内部自动处理响应式
state.user.value = newUser;
// 视图会正确更新
总结:推荐方案


[*] 优先使用 ref 包裹须要整体更新的对象:
定义时用 ref,更新时直接赋值 obj.value = newObj,简便且制止响应式丢失问题。
javascript
const user = ref({}); // 初始化为 ref 对象
user.value = await fetchUser(); // 直接赋值新对象,保持响应式

[*] 若必须使用 reactive 嵌套对象:
使用 Object.assign 合并新属性,或逐个更新属性,确保不替换署理对象的引用。
javascript
Object.assign(state.user, newUser); // 合并属性,保持代理


通过以上方法,既能实现整体对象的更新,又能保持 Vue 3 的响应式特性,制止因引用丢失导致的视图差别步问题。



免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
页: [1]
查看完整版本: Vue 3 reactive 和 ref 区别及 失去响应性问题