最近重温了一下 Vue 3, 把几个最容易踩坑、最影响代码质量的关键 API 整理了一下, 方便以后快速回顾。
1. 组合式 API vs 选项式 API
Vue 3 主推 组合式 API (Composition API) , 核心是用 setup 函数 + 一组响应式 API 组织代码。
js
import { ref, computed, watch, onMounted } from 'vue'
export default {
setup() {
const count = ref(0)
const double = computed(() => count.value * 2)
watch(count, (newVal, oldVal) => {
console.log(`count: ${oldVal} -> ${newVal}`)
})
onMounted(() => console.log('mounted!'))
return { count, double }
}
}
优势:
- 同一业务逻辑的代码可以放在一起 (而不是分散在 data / methods / computed 里)
- 逻辑复用更容易 (用 composable 函数)
- TypeScript 推断更好
2. ref vs reactive ------ 选哪个?
| API | 适用场景 | 注意点 |
|---|---|---|
ref(0) |
基本类型 + 对象/数组都通用 | 访问时要 .value (模板里不用) |
reactive({}) |
仅对象/数组 | 不能整体替换 (state = newObj 会失去响应性) |
实际建议:
- 优先用
ref------ 行为可预测, 一致 - 只有明确知道是对象 且需要直接访问属性 (如
state.user.name) 时用reactive
js
// 反例: reactive 整体替换会丢响应性
const state = reactive({ count: 0 })
state = { count: 1 } // ❌ 失败, 模板不更新
// 正解 1: 用 ref
const state = ref({ count: 0 })
state.value = { count: 1 } // ✅
// 正解 2: 用 Object.assign
Object.assign(state, { count: 1 }) // ✅
3. watch vs watchEffect ------ 怎么选?
watch(source, callback)------ 显式指定监听源, 旧值新值都有watchEffect(callback)------ 自动收集依赖, 立刻执行一次, 简单但拿不到旧值
js
// 场景 1: 监听单个 ref, 需要旧值
watch(count, (newVal, oldVal) => {
console.log(`${oldVal} -> ${newVal}`)
})
// 场景 2: 副作用, 不需要旧值
watchEffect(() => {
console.log(`count is now: ${count.value}`)
})
// 场景 3: 监听 reactive 对象的某个属性
watch(() => state.user.name, (newName) => {
console.log(`name changed to: ${newName}`)
})
实战经验:
- 90% 场景
watch就够用, 显式更可控 - 只有初始化时就要执行 的副作用用
watchEffect - 异步场景记得加
flush: 'post'让 watcher 在 DOM 更新后跑
4. computed 的两个常见坑
坑 1: 试图传参数
js
// ❌ computed 不支持参数
const filtered = computed((id) => items.value.filter(i => i.id === id))
// ✅ 用方法或 derived ref
function getById(id) {
return items.value.find(i => i.id === id)
}
坑 2: 内部有副作用
js
// ❌ computed 应该是纯函数, 副作用会重复执行
const data = computed(() => {
fetch('/api/data') // 不要这样做!
return items.value
})
// ✅ 副作用放 watch / onMounted / 事件处理里
5. defineProps / defineEmits ------ <script setup> 写法
Vue 3.2+ 推荐的写法, 比 defineComponent 简洁很多:
vue
<script setup>
import { ref } from 'vue'
const props = defineProps({
title: { type: String, required: true },
count: { type: Number, default: 0 },
})
const emit = defineEmits(['change', 'submit'])
const localCount = ref(props.count)
</script>
<template>
<h1>{{ title }}</h1>
<button @click="emit('change', localCount)">+</button>
</template>
优势:
- 不需要
return暴露变量 (顶层声明就可用) - 自动按需引入 (
unplugin-auto-import配合更香) - TypeScript 支持更好
6. 响应式 API 的"破局"陷阱
数组 / 对象修改要保留引用
js
const list = ref([1, 2, 3])
// ❌ 不会触发响应
list.value[0] = 99
// ✅ 用 splice
list.value.splice(0, 1, 99)
// ✅ 或用 splice 替代
list.value = [99, 2, 3]
解构 reactive 也会丢响应
js
const state = reactive({ user: { name: 'tom' } })
const { user } = state // ❌ user.name 修改不会触发响应
// ✅
const user = toRef(state, 'user')
// 或
const { user } = toRefs(state)
7. v-model 在自定义组件上的实现
父组件:
vue
<CustomInput v-model="search" />
子组件 (<script setup>):
vue
<script setup>
const props = defineProps({ modelValue: String })
const emit = defineEmits(['update:modelValue'])
</script>
<template>
<input
:value="props.modelValue"
@input="emit('update:modelValue', $event.target.value)"
/>
</template>
进阶 : 用 defineModel 宏 (Vue 3.4+) 可以省一半代码:
vue
<script setup>
const model = defineModel() // 自动双向绑定
</script>
<template>
<input v-model="model" />
</template>
8. 性能优化三件套
| 技巧 | 作用 | 适用 |
|---|---|---|
v-memo |
缓存模板, 只在依赖变化时重渲染 | 大列表 / 复杂组件 |
<KeepAlive> |
缓存组件实例 | 路由切换保留状态 |
shallowRef / shallowReactive |
浅响应, 大对象性能好 | 不需要深层响应 |
vue
<template>
<!-- 只在 value 变化时重渲染 -->
<HeavyList v-memo="[value]" :items="items" />
</template>
9. 总结
Vue 3 相比 Vue 2 主要升级:
- 响应式系统重写 (Proxy)
- Composition API ------ 逻辑复用更灵活
<script setup>------ 模板代码量减半- Teleport / Suspense / Fragment ------ 解决 Vue 2 的痛点
- 更好的 TypeScript 支持
学习建议:
- 不要死记 API, 重点理解响应式原理
- 多写 composable 函数 (类似 React 的 custom hook)
- 大型项目优先用 Pinia 替代 Vuex