--- name: vue-common-gotchas-edge-cases description: Vue 常见陷阱与边界情况 metadata: type: reference --- # Vue 常见陷阱与边界情况 会导致静默失败或难以调试问题的关键 Vue 3 陷阱。 > 基于 [vuejs-ai/skills](https://github.com/vuejs-ai/skills) 的 vue-best-practices。如需全面覆盖(200+ 条规则),请参见上游仓库。 ## 响应式 ### 在脚本中访问 ref() 时始终使用 `.value` **影响:高**——忘记 `.value` 会导致静默失败。 ```ts const count = ref(0) // 错误 count++ // 试图对 ref 对象自增 count = 5 // 重新赋值变量,丢失响应性 items.push(4) // 错误:push 不是函数 // 正确 count.value++ count.value = 5 items.value.push(4) // 在模板中——不需要 .value(Vue 会自动解包) // {{ count }} 可以工作,而不是 {{ count.value }} ``` ### 切勿直接解构 reactive() 对象 **影响:高**——解构会破坏响应连接。 ```ts const state = reactive({ count: 0, name: 'Vue' }) // 错误——解构后的变量失去响应性 const { count, name } = state state.count++ console.log(count) // 仍然是 0! // 正确——使用 toRefs() const { count, name } = toRefs(state) state.count++ console.log(count.value) // 1 // 最佳实践——直接用 ref() 替代 reactive() const count = ref(0) const name = ref('Vue') ``` ### reactive() 的代理标识符风险 ```ts const raw = {} const proxy = reactive(raw) // 错误——比较不同的对象 console.log(proxy === raw) // false // 错误——创建多个代理 const a = reactive({}) const b = reactive(a) // 返回同一个代理 console.log(a === b) // true(同一个对象) // 陷阱——嵌套对象也会被代理 const nested = reactive({ obj: {} }) console.log(nested.obj === nested.obj) // true(同一个代理) ``` ## 计算属性 ### 计算属性的 getter 中不能有副作用 **影响:高**——副作用会破坏响应式模型。 ```ts // 错误——修改状态 const doubled = computed(() => { count.value++ // 副作用! return count.value * 2 }) // 错误——异步操作 const data = computed(async () => { return await fetch('/api') // 副作用! }) // 正确——只做纯计算 const doubled = computed(() => count.value * 2) // 副作用应使用 watch: watch(count, (newVal) => { document.title = `Count: ${newVal}` }) ``` ### 计算属性的返回值是只读的 ```ts const fullName = computed(() => `${first.value} ${last.value}`) // 错误——计算属性的值是只读的 fullName.value = 'John Doe' // 报错! // 正确——使用可写计算属性 const fullName = computed({ get: () => `${first.value} ${last.value}`, set: (val) => { const [f, l] = val.split(' ') first.value = f last.value = l } }) ``` ## 侦听器 ### 清理异步操作以防止竞态条件 **影响:高**——过期的请求可能覆盖新数据。 ```ts const query = ref('') const results = ref([]) // 错误——竞态条件 watch(query, async (q) => { const res = await fetch(`/api?q=${q}`) results.value = await res.json() // 可能覆盖更新的结果! }) // 正确——使用 onWatcherCleanup(Vue 3.5+) watch(query, async (q) => { const controller = new AbortController() onWatcherCleanup(() => controller.abort()) try { const res = await fetch(`/api?q=${q}`, { signal: controller.signal }) results.value = await res.json() } catch (e) { if (e.name !== 'AbortError') throw e } }) // 或使用 onCleanup 参数 watch(query, async (q, oldQ, onCleanup) => { const controller = new AbortController() onCleanup(() => controller.abort()) // ... 与上面相同 }) ``` ### 深度侦听返回同一对象引用 ```ts const obj = reactive({ nested: { count: 0 } }) // 陷阱——深度侦听时 oldValue === newValue watch(obj, (newVal, oldVal) => { console.log(newVal === oldVal) // true!同一个对象 }, { deep: true }) // 如果需要旧值,先克隆: watch( () => structuredClone(obj), (newVal, oldVal) => { /* 现在不同了 */ } ) ``` ## Props ### Props 是只读的——切勿修改 **影响:高**——破坏单向数据流。 ```ts const props = defineProps<{ count: number; user: User }>() // 错误——直接修改 props.count++ // Vue 警告 props.user.name = 'New' // 没有警告但仍然是错误的! // 正确——向父组件 emit const emit = defineEmits(['update:count', 'update-user']) emit('update:count', props.count + 1) emit('update-user', { ...props.user, name: 'New' }) // 或者创建本地副本 const localUser = ref({ ...props.user }) ``` ### 解构后的 Props 不会触发侦听器(3.5 之前) ```ts // 错误(Vue < 3.5) const { count } = defineProps<{ count: number }>() watch(count, () => {}) // 不会触发! // 正确——使用 getter const props = defineProps<{ count: number }>() watch(() => props.count, () => {}) // Vue 3.5+——解构在响应式 props 中可用 const { count } = defineProps<{ count: number }>() watch(() => count, () => {}) // 3.5+ 中可用 ``` ## 生命周期钩子 ### 在 setup 期间同步注册钩子 **影响:高**——异步钩子会静默失败。 ```ts // 错误——在 await 之后注册钩子 async setup() { const data = await fetchData() onMounted(() => {}) // 永远不会执行! } // 错误——在 setTimeout 中注册钩子 setup() { setTimeout(() => { onMounted(() => {}) // 永远不会执行! }, 100) } // 正确——同步注册,异步逻辑放在内部 setup() { onMounted(async () => { const data = await fetchData() }) } ``` ## 模板 ### 切勿在同一元素上同时使用 v-if 与 v-for **影响:高**——Vue 2/3 优先级不同。 ```vue