Vue3 实战避坑:10 个 Composition API 高频错误及修复方案

Vue3 实战避坑:10 个 Composition API 高频错误及修复方案


1) reactive 解构导致失去响应性

  • 现象:对 reactive 返回的对象做 ES 解构后,解构出来的变量不再响应更新。
  • 原因:解构会拿到原始值的快照,脱离代理。
  • 修复:使用 toRefs/toRef 保持引用与代理,或避免直接解构。
ts 复制代码
import { reactive, toRefs, toRef } from 'vue'
const state = reactive({ count: 0, user: { name: 'A' } })
// 错误
const { count } = state
// 正确
const { count: countRef } = toRefs(state)
const nameRef = toRef(state.user, 'name')

2) reactive 顶层替换失效 vs 用 ref 持有整体

  • 现象:reactive 对象整体重新赋值不生效。
  • 原因:reactive 返回的是固定代理,替换变量仅更改引用。
  • 修复:整体替换用 ref 持有对象并改 .value;需要局部更新用 Object.assign
ts 复制代码
import { reactive, ref } from 'vue'
const state = reactive({ list: [1] })
// 错误:不会触发
// state = { list: [2] }
// 正确:
Object.assign(state, { list: [2] })
// 或整体替换:
const holder = ref({ list: [1] })
holder.value = { list: [2] }

3) ref 对象的使用误区(直接改 .value vs 深层属性)

  • 现象:ref({}) 后对 .value 直接替换有效,但对深层属性忘记加 .value
  • 修复:对象整体用 .value = 新对象;深层属性通过 .value.xxx = 修改。
ts 复制代码
import { ref } from 'vue'
const profile = ref({ name: 'A', age: 18 })
profile.value.age = 19
profile.value = { ...profile.value, name: 'B' }

4) watch 源写法错误(未指定 getter 导致性能或触发异常)

  • 现象:watch(state, ...) 监听到大量无关变更或无法精准到字段更新。
  • 修复:监听具体 getter 或 toRef,避免无谓触发;需要深度时设置 { deep: true }
ts 复制代码
import { reactive, toRef, watch } from 'vue'
const state = reactive({ query: '', items: [] })
watch(() => state.query, (nv, ov) => {/*...*/})
watch(toRef(state, 'items'), (nv) => {/*...*/}, { deep: true })

5) watchEffect 误用副作用(无法控制依赖与时机)

  • 现象:在 watchEffect 中做网络请求或写入副作用,导致频繁触发。
  • 修复:副作用用 watch + 明确源;watchEffect 适合派生计算与轻量任务。
ts 复制代码
import { ref, watch, watchEffect } from 'vue'
const id = ref('1')
watch(id, async (nv) => {/* 请求 */}, { flush: 'post' })
watchEffect(() => {/* 轻量派生 */})

6) computed 执行副作用或异步

  • 现象:在 computed 中发请求或修改其他状态,导致不可预测。
  • 修复:computed 必须纯函数且仅依赖响应源;副作用转移到 watch
ts 复制代码
import { computed, ref, watch } from 'vue'
const a = ref(1), b = ref(2)
const sum = computed(() => a.value + b.value)
watch(sum, (v) => {/* 根据值变化触发副作用 */})

7) 更新时机错位(flush 与 nextTick)

  • 现象:watch 中读取 DOM 未更新,或需要在 DOM 更新后执行逻辑。
  • 修复:使用 { flush: 'post' }await nextTick()
ts 复制代码
import { ref, watch, nextTick } from 'vue'
const show = ref(false)
watch(show, async () => { await nextTick(); /* 此时 DOM 已更新 */ }, { flush: 'post' })

8) v-model 子组件协议不一致

  • 现象:父组件使用 v-model 无效或无法双向绑定。
  • 原因:未遵循 modelValueupdate:modelValue 事件约定。
  • 修复:子组件 defineProps/defineEmits 对齐协议。
vue 复制代码
<script setup lang="ts">
import { defineProps, defineEmits } from 'vue'
const props = defineProps<{ modelValue: string }>()
const emit = defineEmits<{ (e: 'update:modelValue', v: string): void }>()
function onInput(v: string) { emit('update:modelValue', v) }
</script>

9) provide/inject 失去响应性

  • 现象:注入后值不随提供者更新;或注入时解构导致丢响应。
  • 修复:提供 ref/reactive,注入后使用原引用或 toRef;避免解构原始值。
ts 复制代码
// provider
import { provide, ref } from 'vue'
const theme = ref('light')
provide('theme', theme)
// consumer
import { inject } from 'vue'
const theme = inject('theme') as typeof theme

10) 清理副作用遗漏(定时器、事件监听、watch 停止)

  • 现象:路由切换或组件销毁后仍触发副作用,出现内存泄漏或异常。
  • 修复:在 onUnmounted 清理;watch 返回停止函数或在回调返回清理函数。
ts 复制代码
import { onUnmounted, watch, ref } from 'vue'
const timer = setInterval(() => {}, 1000)
onUnmounted(() => clearInterval(timer))
const x = ref(0)
const stop = watch(x, (v, o, onCleanup) => { const ctrl = new AbortController(); onCleanup(() => ctrl.abort()) })

额外建议与工具

  • toRef/toRefs:在组合函数中向外暴露字段时保持响应引用。
  • effectScope:将一组副作用统一管理,避免泄漏。
  • 组合式函数规范:输入为响应源或普通值,输出尽量为 ref/computed,副作用内部管理并暴露停止函数。

进阶避坑与修复

shallowReactive/shallowRef 的误用

  • 现象:以为浅响应能自动追踪深层属性,结果更新不触发视图。
  • 修复:深层数据使用 reactive/ref;浅响应配合 triggerRef 手动触发。
ts 复制代码
import { shallowRef, triggerRef } from 'vue'
const data = shallowRef({ list: [] })
data.value.list.push(1)
triggerRef(data)

模板中解构导致失去响应性

  • 现象:在 <script setup> 中对 reactive 解构并直接在模板使用,视图不更新。
  • 修复:使用 toRefs 暴露到模板。
ts 复制代码
import { reactive, toRefs } from 'vue'
const state = reactive({ count: 0 })
const { count } = toRefs(state)

生命周期钩子时机错误

  • 现象:在 onMounted 前读取 DOM 或在 onUpdated 中引入副作用导致频繁触发。
  • 修复:需要 DOM 完成后用 nextTick;副作用搬到 watch 并限制触发源。
ts 复制代码
import { onMounted, nextTick, ref } from 'vue'
const ready = ref(false)
onMounted(async () => { ready.value = true; await nextTick() })

组合式函数的隐式外部依赖

  • 现象:组合式函数内部直接访问组件外部变量,复用失败或不可预测。
  • 修复:所有依赖通过参数传入;输出统一为 ref/computed 与停止句柄。
ts 复制代码
export function usePagination(total: number) {
  const page = ref(1)
  const size = ref(10)
  const pages = computed(() => Math.ceil(total / size.value))
  return { page, size, pages }
}

异步竞态与中断

  • 现象:快速切换参数导致旧请求后返回覆盖新数据。
  • 修复:在 watch 使用 onCleanup 中断,或引入请求层中断控制。
ts 复制代码
import { ref, watch } from 'vue'
const id = ref('1')
watch(id, async (nv, _, onCleanup) => {
  const ctrl = new AbortController()
  onCleanup(() => ctrl.abort())
  await fetch(`/api/${nv}`, { signal: ctrl.signal })
})

事件监听与路由切换泄漏

  • 现象:在组合函数或组件中绑定窗口事件,路由切换后仍触发。
  • 修复:返回停止函数或在 onUnmounted 清理。
ts 复制代码
import { onMounted, onUnmounted } from 'vue'
function useResize() {
  const handler = () => {}
  onMounted(() => window.addEventListener('resize', handler))
  onUnmounted(() => window.removeEventListener('resize', handler))
}

测试与验证清单

  • 响应性:对 reactive/ref 的字段更新是否触发视图与计算值更新。
  • 副作用:watch/watchEffect 是否仅针对预期源触发,是否有清理。
  • 时机:DOM 依赖逻辑是否在 post 阶段或 nextTick 后执行。
  • 组合函数:输入输出是否纯净、是否暴露停止函数、内部是否自清理。
  • 路由:切换后计时器/事件监听是否停止,内存增长是否可控。

性能与稳定性建议

  • 优先使用 computed 做派生,减少 watch;对昂贵计算使用缓存与拆分。
  • 列表与表单场景避免深度 watch;精确监听源或使用局部 ref
  • 合理使用懒加载与分包;避免在 setup 中做沉重同步工作。

组合式函数设计准则

  • 输入明确与参数化,输出统一为响应引用;副作用内部管理并可停止。
  • 不读组件外部全局状态,统一从参数或依赖注入获取。
  • 文档化使用方式与限制,提供最小复用单元与扩展点。
相关推荐
郑板桥301 小时前
如何自定义一个MCP服务器:从零到一的完整指南
前端·vscode
BlackWolfSky1 小时前
Web基础
前端
b***66611 小时前
【慕伏白教程】Zerotier 连接与简单配置
android·前端·后端
我爱学习_zwj1 小时前
《第七章》TS工程基础:检查指令与类型声明实战
前端·typescript
关于不上作者榜就原神启动那件事1 小时前
心跳机制详解
java·前端·servlet
杀死那个蝈坦1 小时前
Redis 持久化 主从 哨兵 分片集群
前端·bootstrap·html
eason_fan1 小时前
什么是模块联邦?(Module Federation)
前端·javascript·前端工程化
J总裁的小芒果1 小时前
el-table 假数据合并
javascript·vue.js·elementui
W***D4551 小时前
SpringBoot + vue 管理系统
vue.js·spring boot·后端