踩坑笔记 # vue # 前端已发布

Vue 3 :常见踩坑记录与避坑指南

Vue 3 凭借 Composition API、更高效的 Proxy 响应式系统 和 TypeScript 支持,已经成为现代前端开发的主流选择。然而,从 Vue 2 转换到 Vue 3 的过程中,或者在日常开发 Vue 3 项目时,由于对新的响应式机制和 API 理解不够深入,开发者极易陷入一些隐蔽的“坑”中。

2025.01.254 分钟阅读
Vue 3 :常见踩坑记录与避坑指南
Vue 3 :常见踩坑记录与避坑指南 · Cover

#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 避坑核心口诀

  1. 响应式选型:优先选择 ref(),简单直观少出坑;只有需要对大型对象进行分组建模时才用 reactive()。
  2. 解构保响应:解构 reactive 或 props 必用 toRefs()。
  3. Watch 指针:监听对象某个具体 key 时,一定要包裹成函数 () => obj.key。
  4. 模板渲染防空:深层嵌套异步数据,一定要加可选链 ?. 或者 v-if。
  5. 副作用清理:全局监听与定时器,onUnmounted 阶段一定要移除。
#vue #前端
ZZC

ZZC

写代码,也写一些生活。