#Vue 3 凭借 Composition API、更高效的 Proxy 响应式系统 和 TypeScript 支持,已经成为现代前端开发的主流选择。然而,从 Vue 2 转换到 Vue 3 的过程中,或者在日常开发 Vue 3 项目时,由于对新的响应式机制和 API 理解不够深入,开发者极易陷入一些隐蔽的“坑”中。
坑点一:reactive 变量被直接赋值导致响应式丢失
1. 现象描述
在处理对象或数组时,喜欢使用 reactive。然而,当从后端获取到新数据并直接赋值给 reactive 对象时,页面却不再更新了。
import { reactive } from 'vue';
// 初始定义响应式对象
let userList = reactive([1, 2, 3]);
const fetchData = () => {
// ❌ 错误做法:直接重新赋值,这会导致原来的 Proxy 对象引用断开,响应式失效!
userList = [4, 5, 6];
};
2. 原因分析
reactive 返回的是一个原生对象的 Proxy 代理。如果将一个全新的数组或对象赋值给声明为 let 的变量,相当于让该变量指向了一个新的内存地址,旧 Proxy 对象的引用就被丢弃掉了,模板依然绑定的是旧的 Proxy。
3. 解决方案
- 方案 1:改用
ref(强烈推荐)ref可以处理任何数据类型,对重新赋值友好,赋值时通过.value进行,不会破坏响应式引用。import { ref } from 'vue'; const userList = ref([1, 2, 3]); const fetchData = () => { // ✅ 推荐做法:通过 .value 重新赋值 userList.value = [4, 5, 6]; }; - 方案 2:作为对象的属性或使用
Object.assign如果一定要用reactive,可以将其包裹在一个大对象中,或者通过修改对象内部属性/修改数组原方法来更新。// 方式 A: 修改对象属性 const state = reactive({ list: [1, 2, 3] }); state.list = [4, 5, 6]; // ✅ 正常触发更新 // 方式 B: 清空并 push (针对数组) const list = reactive([1, 2, 3]); list.length = 0; list.push(...[4, 5, 6]); // ✅ 正常触发更新
坑点二:对 reactive 或 ref 进行解构导致响应式丢失
1. 现象描述
为了简化代码,开发者常对 reactive 对象或组件的 props 进行解构,结果发现解构出来的变量不再具有响应性。
import { reactive } from 'vue';
const userInfo = reactive({ name: '张三', age: 18 });
// ❌ 错误做法:直接解构,name 和 age 会变成普通的 JavaScript 基本类型值
const { name, age } = userInfo;
const updateName = () => {
name = '李四'; // 页面不会更新!
};
2. 解决方案:使用 toRefs 或 toRef
通过 toRefs 可以将 reactive 对象的每一个属性转换为对应的 ref,从而保留响应式连接。
import { reactive, toRefs } from 'vue';
const userInfo = reactive({ name: '张三', age: 18 });
// ✅ 正确做法:使用 toRefs 进行解构
const { name, age } = toRefs(userInfo);
const updateName = () => {
name.value = '李四'; // ✅ 页面正常更新
};
坑点三:在 <script setup> 中误用 watch 监听 reactive 对象的深层属性
1. 现象描述
使用 watch 监听一个 reactive 对象的某个特定属性时,更改了属性值但侦听器没有触发,或者监听到新旧值(newValue 和 oldValue)完全相同。
import { reactive, watch } from 'vue';
const state = reactive({
user: {
name: '张三',
age: 18
}
});
// ❌ 错误做法:直接监听普通属性值 (例如 state.user.name),这等同于传了一个静态字符串
watch(state.user.name, (newVal) => {
console.log('Name changed:', newVal); // 永远不会触发!
});
2. 解决方案:使用 Getter 函数 (Getter Function)
监听 reactive 对象的某个特定属性时,必须使用一个返回该属性值的 getter 函数:
// ✅ 正确做法:使用函数形式传递监听源
watch(
() => state.user.name,
(newVal, oldVal) => {
console.log(`Name 从 ${oldVal} 变成了 ${newVal}`);
}
);
// 如果监听的是一个深层嵌套对象,且需要开启深度监听
watch(
() => state.user,
(newVal) => {
console.log('User 改动了', newVal);
},
{ deep: true } // ✅ 明确开启 deep
);
坑点四:异步数据加载导致模板读取 undefined 报错
1. 现象描述
前端从后端异步获取数据,渲染包含嵌套深度的对象属性时,页面直接报错甚至崩溃。
<template>
<!-- ❌ 报错:TypeError: Cannot read properties of undefined (reading 'avatar') -->
<div>
<img :src="userInfo.profile.avatar" />
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue';
const userInfo = ref({}); // 初始值为空对象,没有 profile 属性
onMounted(async () => {
const res = await fetchUserData();
userInfo.value = res.data; // profile 属性在异步响应后才存在
});
</script>
2. 解决方案
- 方案 1:可选链操作符(Optional Chaining
?.) 最简便优雅的防报错方式。<img :src="userInfo.profile?.avatar" /> - 方案 2:利用
v-if指令进行渲染条件判断<div v-if="userInfo.profile"> <img :src="userInfo.profile.avatar" /> </div> - 方案 3:给
ref初始化完整的默认数据结构const userInfo = ref({ profile: { avatar: '' } });
坑点五:组件销毁后,watch 或全局事件监听未手动清理导致内存泄漏
1. 现象描述
在组件内部使用了全局事件监听(如 window.addEventListener)或全局状态库(Pinia/Vuex)的侦听,当组件被卸载/切换页面后,事件仍然被频繁触发,产生内存泄漏。
2. 解决方案:在生命周期钩子中清理副作用
虽然 Vue 3 在组件卸载时会自动销毁绑定在组件生命周期内的 watch,但对于全局 DOM 事件、setInterval 定时器、或是显式通过 scope.stop() 停止的监听,必须在 onUnmounted 中手动清理。
import { onMounted, onUnmounted } from 'vue';
const handleResize = () => {
console.log('窗口尺寸变更');
};
onMounted(() => {
window.addEventListener('resize', handleResize);
});
// ✅ 正确做法:组件销毁时必须同步移除监听
onUnmounted(() => {
window.removeEventListener('resize', handleResize);
});
(提示:推荐使用 @vueuse/core 提供的 useEventListener 等组合式 API,它们会在组件卸载时自动帮我们做清理作)
总结:Vue 3 避坑核心口诀
- 响应式选型:优先选择
ref(),简单直观少出坑;只有需要对大型对象进行分组建模时才用reactive()。 - 解构保响应:解构
reactive或props必用toRefs()。 - Watch 指针:监听对象某个具体 key 时,一定要包裹成函数
() => obj.key。 - 模板渲染防空:深层嵌套异步数据,一定要加可选链
?.或者v-if。 - 副作用清理:全局监听与定时器,
onUnmounted阶段一定要移除。
