Vue 3 Composition API 最佳实践

FreeGuideOnline 最新 2026-07-13

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 computedwatch 的精简用法

  • 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 解决就不用 watchwatchEffect 适合无需旧值的场景。

二、逻辑复用神器:组合函数(Composables)

2.1 命名约定与文件组织

  • 组合函数名必须以 use 开头,如 useMouseuseFetch
  • 文件放在 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 / onBeforeMount
  • mounted / onMounted
  • beforeUpdate / onBeforeUpdate
  • updated / onUpdated
  • beforeUnmount / onBeforeUnmount
  • unmounted / onUnmounted 注意没有 beforeCreatecreated,因为 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(除 datamethods 等冲突部分外)。但全新项目推荐完全使用 Composition API。

5.2 常用 Options 的对应关系

  • dataref / reactive
  • computedcomputed()
  • methods → 普通函数
  • watchwatch() / watchEffect()
  • mountedonMounted()
  • props 通过 defineProps() 声明,emits 通过 defineEmits()

最佳实践:新组件用 <script setup>,老组件可保持不变,除非需要复用逻辑。

六、与 TypeScript 的完美结合

6.1 类型推导与显式注解

Vue 3 对 TS 支持极佳,refcomputed 都能自动推导类型。复杂结构可手动标注:

interface User { name: string; age: number }
const user = ref<User | null>(null)

6.2 PropsEmits 的类型声明

<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