Vue 3 Composition API 最佳实践
vue
传统 `setup()` 仍可用于兼容场景,但最佳实践是拥抱 `<script setup>`。
### 1.2 响应式数据:`ref` 与 `reactive` 的选择
- **`ref`**:适合所有基本类型和需要替换整个值的对象。在模板中会自动解包,逻辑清晰。
```js
const user = ref({ name: 'Alice' })
// 更新整个对象
user.value = { name: 'Bob' }
reactive:适合复杂对象(如表单数据),但不可直接替换整个对象,且解构会丢失响应式。const state = reactive({ count: 0, list: [] }) // 错误:替换整体会失去响应式 // state = reactive({ ... })
最佳实践:优先使用 ref,尤其是当数据需要被重新赋值或传递给组合函数时。对于固定结构的对象集合,可以使用 reactive,但保持整体引用不变。
1.3 computed 与 watch 的精简用法
computed应保持纯粹,无副作用。const double = computed(() => count.value * 2)watch监听ref或多个源时,使用数组,并善用watchEffect处理简单追踪。watch([count, name], ([newCount, newName]) => { console.log('Count or name changed') }) // 立即执行并自动追踪依赖 watchEffect(() => { console.log(`Count is ${count.value}`) })
最佳实践:能用 computed 解决就不用 watch。watchEffect 适合无需旧值的场景。
二、逻辑复用神器:组合函数(Composables)
2.1 命名约定与文件组织
- 组合函数名必须以
use开头,如useMouse、useFetch。 - 文件放在
src/composables/目录下,每个函数一个文件。 - 函数返回一个非响应式对象(或
toRefs包裹),方便解构使用。
// composables/useCounter.js
import { ref, computed } from 'vue'
export function useCounter(initialValue = 0) {
const count = ref(initialValue)
const double = computed(() => count.value * 2)
function increment() { count.value++ }
return { count, double, increment }
}
2.2 接受响应式参数
组合函数应能接受 ref 或普通值作为参数,内部使用 unref 安全取值。
import { unref } from 'vue'
export function useTitle(newTitle) {
const title = unref(newTitle)
document.title = title
}
2.3 清理副作用
在组合函数中使用 watch 或事件监听时,务必在组件卸载时清理。
export function useEventListener(target, event, handler) {
onMounted(() => {
target.addEventListener(event, handler)
})
onUnmounted(() => {
target.removeEventListener(event, handler)
})
}
最佳实践:组合函数应保持单一职责,输入输出清晰,并提供清理逻辑。
三、生命周期与依赖注入
3.1 生命周期钩子对照
setup 中直接导入钩子函数,名称前缀 on:
beforeMount/onBeforeMountmounted/onMountedbeforeUpdate/onBeforeUpdateupdated/onUpdatedbeforeUnmount/onBeforeUnmountunmounted/onUnmounted注意没有beforeCreate和created,因为setup本身就是围绕这两个阶段执行的。
最佳实践:将相关逻辑放在一起,即使它们散落在不同生命周期。例如,数据获取和滚动监听应各成一个组合函数,而不是将所有 onMounted 堆在一处。
3.2 使用 provide / inject 实现依赖注入
<!-- 父组件 -->
<script setup>
import { provide, ref } from 'vue'
const theme = ref('dark')
provide('theme', theme)
</script>
<!-- 子组件 -->
<script setup>
import { inject } from 'vue'
const theme = inject('theme', 'light') // 默认值
</script>
最佳实践:用 Symbol 作为注入键名以避免命名冲突,并将 provide 逻辑封装在组合函数中。
四、模板引用与 DOM 操作
4.1 模板引用的统一写法
在 <script setup> 中,模板引用同名变量会被自动绑定。
<template>
<input ref="inputRef" />
</template>
<script setup>
import { ref, onMounted } from 'vue'
const inputRef = ref(null)
onMounted(() => {
inputRef.value?.focus()
})
</script>
最佳实践:在 onMounted 中访问 DOM,因为此时模板已挂载。
4.2 组件引用与 defineExpose
子组件需要暴露方法或属性时使用 defineExpose。
<!-- Child.vue -->
<script setup>
const reset = () => { /* ... */ }
defineExpose({ reset })
</script>
父组件通过模板引用调用。
五、与 Options API 的协作与迁移
5.1 渐进式迁移策略
在一个组件中可以同时使用 setup() 和 Options API(除 data、methods 等冲突部分外)。但全新项目推荐完全使用 Composition API。
5.2 常用 Options 的对应关系
data→ref/reactivecomputed→computed()methods→ 普通函数watch→watch()/watchEffect()mounted→onMounted()props通过defineProps()声明,emits通过defineEmits()
最佳实践:新组件用 <script setup>,老组件可保持不变,除非需要复用逻辑。
六、与 TypeScript 的完美结合
6.1 类型推导与显式注解
Vue 3 对 TS 支持极佳,ref 和 computed 都能自动推导类型。复杂结构可手动标注:
interface User { name: string; age: number }
const user = ref<User | null>(null)
6.2 Props 和 Emits 的类型声明
<script setup lang="ts">
const props = defineProps<{
title: string
count?: number
}>()
const emit = defineEmits<{
(e: 'update', value: number): void
}>()
</script>
最佳实践:始终提供类型声明,充分利用 IDE 的智能提示。
6.3 泛型组合函数
export function useArray<T>(initial: T[]) {
const arr = ref(initial) as Ref<T[]>
// ...
return { arr }
}
七、大型组件的代码组织
7.1 按功能拆分组合函数
避免将所有逻辑写在 <script setup> 顶层,应将数据、业务逻辑、DOM 操作提取为独立组合函数。
<script setup>
import { useUserProfile } from './useUserProfile'
import { useScrollAnimation } from './useScrollAnimation'
const { user, updateProfile } = useUserProfile()
const { animate } = useScrollAnimation()
</script>
7.2 状态管理的选择:Pinia
对于跨组件的全局状态,推荐使用 Pinia(官方新一代状态管理库)。它本身就是用 Composition API 风格构建的。
八、常见陷阱与避免方法
8.1 解构 reactive 导致响应式丢失
const state = reactive({ count: 0 })
const { count } = state // ❌ count 现在是普通数值,非响应式
解决:使用 toRefs
const { count } = toRefs(state) // ✅ count 是 Ref
8.2 在 setup 外使用组合函数
组合函数内部若使用了 onMounted 等生命周期钩子,必须确保在组件的 setup 同步调用链中执行,否则会报错。
8.3 不必要的响应式包装
不要将不会改变的数据设为响应式,例如:
const options = { payType: ['wechat', 'alipay'] } // 静态常量,不需要 ref