人人都会AI编程

24.4 常见反模式

更新时间:2026-07-11

反模式是指那些看似能解决问题,但实际会引入隐患、降低可维护性或造成性能问题的写法。识别并避免这些反模式,是提升 Vue 代码质量的关键一步。


一、v-ifv-for 同时使用

这是 Vue 新手最容易踩的坑之一。在同一元素上同时使用 v-ifv-for 时,v-for 的优先级更高,意味着 v-if 会在每次循环中重复执行。

<!-- ❌ 错误:v-if 在每次迭代中都会执行 -->
<li v-for="item in list" v-if="item.isActive" :key="item.id">
  {{ item.name }}
</li>

问题所在

  • 即使 list 中只有少数几个 isActivetrue 的项,Vue 仍然会遍历整个数组,对每一项执行 v-if 判断。
  • 这种写法会让模板意图模糊,也容易忽略性能损耗(尤其列表很长时)。

正确做法

  • 先用计算属性过滤数据,再对过滤后的结果进行迭代。
  • 如果需要控制整个列表是否展示,应把 v-if 放在外层容器上。
<!-- ✅ 正确:用计算属性预先过滤 -->
<template v-if="activeItems.length">
  <li v-for="item in activeItems" :key="item.id">
    {{ item.name }}
  </li>
</template>
const activeItems = computed(() => list.value.filter(item => item.isActive))

这样既保证了 v-if 只执行一次(在计算属性中),也让模板职责更清晰:循环只负责展示,不再承担过滤逻辑。


二、滥用 watch 导致逻辑混乱

watch 是监听数据变化的强大工具,但过度使用会让组件逻辑难以追踪,甚至造成数据流的“循环依赖”。

典型反模式 1:把 watch 当计算属性使

// ❌ 错误:用 watch 手动同步派生状态
const firstName = ref('')
const lastName = ref('')
const fullName = ref('')

watch([firstName, lastName], ([first, last]) => {
  fullName.value = first + ' ' + last
})

这种写法不仅代码冗余,还额外维护了一个响应式变量 fullName。一旦在别处也修改了 fullName,就很难理清数据是从哪里“衍生的”。

✅ 正确:直接使用 computed

const fullName = computed(() => `${firstName.value} ${lastName.value}`)

computed 自带缓存,且逻辑单一、可阅读性强。凡是能从现有数据推导出来的值,都应该用 computed,而非 watch


典型反模式 2:watch 中触发另一个 watch 的操作链

// ❌ 错误:watch 依赖链
watch(a, (newA) => { b.value = newA + 1 })
watch(b, (newB) => { c.value = newB * 2 })

a 变化时,b 会改变,接着 c 也改变。这种级联式的响应很容易变成“意料之外的副作用堆栈”,调试时根本摸不清起因。

✅ 解决思路

  • 尽量将依赖关系合并为 computedc = computed(() => (a.value + 1) * 2))。
  • 若确实需要命令式操作(如发请求),也应把逻辑收敛在一个 watch 或一个方法中,避免触发链式反应。

典型反模式 3:watch 深度监听大对象,不加任何限定

// ❌ 可能引发性能问题
watch(someLargeObject, callback, { deep: true })

深度监听会递归遍历对象的所有嵌套属性,当对象结构复杂时,每次修改都可能触发大量的依赖收集和回调执行,性能开销肉眼可见。

✅ 改进

  • 尽可能精确监听具体路径:watch(() => someLargeObject.a.b, callback)
  • 若必须使用 deep,考虑用 shallowRefmarkRaw 避免深层响应。

三、内存泄漏的“三剑客”

在单页应用中,组件会频繁创建和销毁。如果某些资源在组件卸载时未被清理,就会持续占用内存,形成泄漏。

1. 忘记清除定时器 / setInterval

// ❌ 错误:组件销毁后定时器仍在运行
mounted() {
  this.timer = setInterval(() => { ... }, 1000)
}

即使组件已从页面移除,这个 setInterval 仍然会在后台执行,其内部引用的组件实例也不会被回收。

✅ 正确:在 beforeUnmount(Vue 3)或 beforeDestroy(Vue 2)中清理:

const timer = ref(null)
onMounted(() => {
  timer.value = setInterval(fetchData, 5000)
})
onBeforeUnmount(() => {
  clearInterval(timer.value)
})

2. 全局事件监听未解绑

// ❌ 错误:addEventListener 后未在组件销毁时移除
mounted() {
  window.addEventListener('resize', this.handleResize)
}

当组件被销毁时,handleResize 仍在监听全局事件,当事件触发时它会尝试更新一个已经不存在的组件状态,轻则控制台报错,重则内存泄漏。

✅ 正确:在 beforeUnmount 中移除:

onMounted(() => window.addEventListener('resize', handleResize))
onBeforeUnmount(() => window.removeEventListener('resize', handleResize))

3. 第三方库实例未销毁

使用了图表(ECharts)、编辑器(Quill)、轮播(Swiper)等第三方库时,如果只创建实例而忘记调用其销毁方法,结果和上面的定时器、事件监听一样。

// ❌ 错误:echarts 实例未销毁
let chart = null
onMounted(() => {
  chart = echarts.init(container.value)
  chart.setOption({...})
})

✅ 正确:主动调用 dispose,并置空引用:

onBeforeUnmount(() => {
  chart?.dispose()
  chart = null
})

四、其他值得警惕的写法

  • 直接修改 props(破坏单向数据流)

props 是只读的,试图在子组件内部直接修改 prop 值会引发警告。需要用 emit 通知父组件修改,或在子组件内部用 ref 做临时副本。

  • reactive 对象整体替换

const state = reactive({ count: 1 }) 之后,如果执行 state = reactive({ count: 2 }),会丢失响应式(因为引用变了)。应替换为 Object.assign(state, { count: 2 }) 或使用 ref 包裹。

  • 解构 reactive 丢失响应式

const { count } = reactive({ count: 1 }) 会让 count 变成普通数字。正确做法是使用 toRefs 或直接访问 state.count

  • 滥用 v-show 在大量元素上

v-show 只是切换 display,元素始终存在于 DOM 中。如果有数百个隐藏的复杂结点,会白白消耗内存和渲染资源。此类场景改用 v-if 才是合理的。


识别并规避这些反模式,能让你的 Vue 应用从“能跑”升级为“健壮、可维护”。 好的代码不仅满足功能需求,更经得起时间与团队的考验。