跳转到内容

Vue 3 Composition API 最佳实践 2026

Vue 3 Composition API

💚 组合胜于继承。 Vue 3 的 Composition API 彻底改变了 Vue 组件的编写方式。相比 Options API,它提供了更好的逻辑复用、更灵活的代码组织和更优秀的 TypeScript 支持。本文带你从入门到精通 Composition API,掌握 Vue 3 开发的最佳实践。

本文将带你系统掌握:

  • ✅ setup 与 <script setup> 语法
  • ✅ ref vs reactive 响应式系统
  • ✅ computed 与 watch 最佳实践
  • ✅ Composables 组合式函数设计
  • ✅ 依赖注入(provide/inject)
  • ✅ 自定义指令与插件
  • ✅ 性能优化技巧
  • ✅ 项目架构最佳实践

一、从 Options API 到 Composition API

1.1 为什么需要 Composition API

问题Options APIComposition API
逻辑复用mixins(命名冲突)Composables(清晰)
代码组织按选项分散按功能聚合
类型推断优秀
Tree-shaking
代码压缩

1.2 对比示例

Options API:

vue
<script>
export default {
  data() {
    return { count: 0, name: '' }
  },
  computed: {
    double() { return this.count * 2 }
  },
  methods: {
    increment() { this.count++ }
  },
  mounted() {
    console.log('mounted')
  }
}
</script>

Composition API(<script setup>):

vue
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'

const count = ref(0)
const name = ref('')
const double = computed(() => count.value * 2)

function increment() {
  count.value++
}

onMounted(() => {
  console.log('mounted')
})
</script>

二、响应式系统

2.1 ref

ref 用于基本类型和需要整体替换的值:

typescript
import { ref } from 'vue'

const count = ref(0)
const name = ref('Vue')
const items = ref<string[]>([])

// 访问和修改需要 .value
count.value++
name.value = 'Vue 3'
items.value.push('new item')

// 模板中自动解包,不需要 .value
// <div>{{ count }}</div>

2.2 reactive

reactive 用于对象类型,返回深层响应式代理:

typescript
import { reactive } from 'vue'

const state = reactive({
  user: { name: 'Alice', age: 25 },
  items: []
})

// 直接访问,不需要 .value
state.user.name = 'Bob'
state.items.push({ id: 1 })

2.3 ref vs reactive 选择

特性refreactive
适用类型任意对象/数组
访问方式.value直接
解构保持响应式失去响应式
替换直接赋值需要 Object.assign
推荐场景基本类型、需替换表单、复杂对象

reactive 解构问题:

typescript
const state = reactive({ count: 0, name: 'Vue' })

// 解构后失去响应式!
const { count, name } = state  // 不是响应式的

// 使用 toRefs 保持响应式
import { toRefs } from 'vue'
const { count, name } = toRefs(state)  // 响应式

2.4 其他响应式 API

typescript
import { 
  shallowRef, shallowReactive,
  readonly, shallowReadonly,
  toRef, toRefs, toRaw,
  isRef, isReactive, isProxy
} from 'vue'

// shallowRef:只有 .value 是响应式的
const obj = shallowRef({ count: 0 })
obj.value.count = 1  // 不触发更新
obj.value = { count: 1 }  // 触发更新

// readonly:只读代理
const original = reactive({ count: 0 })
const copy = readonly(original)

// toRaw:获取原始对象
const raw = toRaw(reactiveObj)

// isRef:判断是否为 ref
console.log(isRef(count))  // true

三、computed 与 watch

3.1 computed

typescript
import { ref, computed } from 'vue'

const firstName = ref('John')
const lastName = ref('Doe')

// 只读计算属性
const fullName = computed(() => `${firstName.value} ${lastName.value}`)

// 可写计算属性
const fullNameWritable = computed({
  get: () => `${firstName.value} ${lastName.value}`,
  set: (newVal: string) => {
    [firstName.value, lastName.value] = newVal.split(' ')
  }
})

3.2 computed 最佳实践

typescript
// 避免在 computed 中产生副作用
// 错误示例
const bad = computed(() => {
  api.fetchData()  // 不要在 computed 中做异步操作
  return data.value
})

// 正确:用 watch 处理副作用
watch(source, () => {
  api.fetchData()
})

3.3 watch

typescript
import { ref, watch, watchEffect } from 'vue'

const count = ref(0)
const obj = reactive({ nested: { value: 0 } })

// 监听 ref
watch(count, (newVal, oldVal) => {
  console.log(`${oldVal} -> ${newVal}`)
})

// 监听 reactive 属性(需要 getter 函数)
watch(
  () => obj.nested.value,
  (newVal, oldVal) => {
    console.log(`${oldVal} -> ${newVal}`)
  }
)

// 深度监听
watch(
  obj,
  (newVal) => {
    console.log('对象变化', newVal)
  },
  { deep: true }
)

// 立即执行
watch(
  count,
  (newVal) => {
    console.log('立即执行', newVal)
  },
  { immediate: true }
)

3.4 watchEffect

typescript
import { watchEffect, ref } from 'vue'

const count = ref(0)
const name = ref('Vue')

// 自动追踪依赖,立即执行
watchEffect(() => {
  console.log(`count: ${count.value}, name: ${name.value}`)
})

// 清理副作用
watchEffect((onCleanup) => {
  const timer = setTimeout(() => {
    console.log(count.value)
  }, 1000)
  
  onCleanup(() => {
    clearTimeout(timer)
  })
})

3.5 watch vs watchEffect

特性watchwatchEffect
依赖追踪显式指定自动追踪
旧值可获取不提供
立即执行immediate默认立即
适用于需要新旧值对比只需执行副作用

四、Composables 组合式函数

4.1 什么是 Composables

Composables 是以 use 开头的函数,封装可复用的有状态逻辑:

typescript
// composables/useCounter.ts
import { ref, computed } from 'vue'

export function useCounter(initialValue = 0) {
  const count = ref(initialValue)
  const double = computed(() => count.value * 2)
  
  function increment() { count.value++ }
  function decrement() { count.value-- }
  function reset() { count.value = initialValue }
  
  return { count, double, increment, decrement, reset }
}
vue
<script setup lang="ts">
import { useCounter } from '@/composables/useCounter'

const { count, double, increment } = useCounter(10)
</script>

4.2 实用 Composable:useMouse

typescript
// composables/useMouse.ts
import { ref, onMounted, onUnmounted } from 'vue'

export function useMouse() {
  const x = ref(0)
  const y = ref(0)
  
  function update(event: MouseEvent) {
    x.value = event.pageX
    y.value = event.pageY
  }
  
  onMounted(() => {
    window.addEventListener('mousemove', update)
  })
  
  onUnmounted(() => {
    window.removeEventListener('mousemove', update)
  })
  
  return { x, y }
}

4.3 实用 Composable:useFetch

typescript
// composables/useFetch.ts
import { ref, watchEffect, type Ref } from 'vue'

export function useFetch<T>(url: Ref<string> | string) {
  const data = ref<T | null>(null) as Ref<T | null>
  const error = ref<string | null>(null)
  const loading = ref(false)
  
  async function doFetch() {
    loading.value = true
    error.value = null
    data.value = null
    
    try {
      const response = await fetch(typeof url === 'string' ? url : url.value)
      if (!response.ok) throw new Error(response.statusText)
      data.value = await response.json()
    } catch (e) {
      error.value = (e as Error).message
    } finally {
      loading.value = false
    }
  }
  
  // 如果 url 是 ref,自动重新请求
  if (typeof url !== 'string') {
    watchEffect(doFetch)
  } else {
    doFetch()
  }
  
  return { data, error, loading, refresh: doFetch }
}

4.4 实用 Composable:useLocalStorage

typescript
// composables/useLocalStorage.ts
import { ref, watch, type Ref } from 'vue'

export function useLocalStorage<T>(
  key: string,
  defaultValue: T
): Ref<T> {
  const stored = localStorage.getItem(key)
  const data = ref<T>(
    stored ? JSON.parse(stored) : defaultValue
  ) as Ref<T>
  
  watch(
    data,
    (newVal) => {
      localStorage.setItem(key, JSON.stringify(newVal))
    },
    { deep: true }
  )
  
  return data
}
vue
<script setup>
import { useLocalStorage } from '@/composables/useLocalStorage'

const theme = useLocalStorage('theme', 'light')
const settings = useLocalStorage('settings', { sidebar: true, compact: false })
</script>

4.5 Composable 设计原则

  1. 命名规范:以 use 开头
  2. 返回值:返回 ref 或 reactive,不要返回普通值
  3. 生命周期:在函数内注册 onMounted
  4. 输入参数:支持 ref 和普通值
  5. 无副作用:不在 composable 中直接操作 DOM
  6. 类型安全:使用泛型

五、依赖注入

5.1 provide / inject

typescript
// 父组件
import { provide, ref } from 'vue'
import type { InjectionKey, Ref } from 'vue'

// 定义 injection key(类型安全)
const countKey: InjectionKey<Ref<number>> = Symbol('count')

const count = ref(0)
provide(countKey, count)
typescript
// 子组件
import { inject } from 'vue'

const count = inject(countKey)  // 类型: Ref<number> | undefined
// 带默认值
const count = inject(countKey, ref(0))

5.2 封装 provider

typescript
// composables/useTheme.ts
import { ref, provide, inject, type InjectionKey, type Ref } from 'vue'

const themeKey: InjectionKey<Ref<string>> = Symbol('theme')

export function provideTheme(initial: string = 'light') {
  const theme = ref(initial)
  provide(themeKey, theme)
  return theme
}

export function useTheme() {
  const theme = inject(themeKey)
  if (!theme) {
    throw new Error('useTheme must be used within a ThemeProvider')
  }
  return theme
}
vue
<!-- App.vue -->
<script setup>
import { provideTheme } from '@/composables/useTheme'
const theme = provideTheme('dark')
</script>

<!-- Child.vue -->
<script setup>
import { useTheme } from '@/composables/useTheme'
const theme = useTheme()
</script>

六、生命周期钩子

typescript
import {
  onBeforeMount, onMounted,
  onBeforeUpdate, onUpdated,
  onBeforeUnmount, onUnmounted,
  onErrorCaptured, onActivated, onDeactivated
} from 'vue'

// 组件挂载
onBeforeMount(() => console.log('before mount'))
onMounted(() => console.log('mounted'))

// 组件更新
onBeforeUpdate(() => console.log('before update'))
onUpdated(() => console.log('updated'))

// 组件卸载(清理资源)
onBeforeUnmount(() => console.log('before unmount'))
onUnmounted(() => {
  console.log('unmounted')
  // 清理定时器、事件监听器等
  clearInterval(timer)
  window.removeEventListener('resize', handler)
})

// 错误捕获
onErrorCaptured((err, instance, info) => {
  console.error('错误:', err)
  return false  // 阻止继续传播
})

七、<script setup> 最佳实践

7.1 Props 和 Emits

vue
<script setup lang="ts">
// Props 定义
interface Props {
  title: string
  count?: number
  items?: string[]
}

const props = withDefaults(defineProps<Props>(), {
  count: 0,
  items: () => []
})

// Emits 定义
const emit = defineEmits<{
  (e: 'update', value: string): void
  (e: 'delete', id: number): void
}>()

function handleClick() {
  emit('update', 'new value')
}
</script>

7.2 defineModel(Vue 3.4+)

vue
<!-- 双向绑定变得简单 -->
<script setup lang="ts">
const model = defineModel<string>()
</script>

<template>
  <input v-model="model" />
</template>

<!-- 父组件使用 -->
<!-- <MyInput v-model="text" /> -->

7.3 defineExpose

vue
<script setup>
import { ref } from 'vue'

const count = ref(0)
function reset() { count.value = 0 }

// 暴露给父组件通过 ref 访问
defineExpose({ count, reset })
</script>
vue
<!-- 父组件 -->
<script setup>
import { ref } from 'vue'
import Child from './Child.vue'

const childRef = ref()
// childRef.value?.count
// childRef.value?.reset()
</script>

7.4 useSlots 和 useAttrs

vue
<script setup>
import { useSlots, useAttrs } from 'vue'

const slots = useSlots()
const attrs = useAttrs()

console.log(slots.default)
console.log(attrs.class)
</script>

八、性能优化

8.1 v-once 和 v-memo

vue
<template>
  <!-- v-once: 只渲染一次 -->
  <div v-once>{{ expensiveComputed() }}</div>
  
  <!-- v-memo: 依赖不变时跳过更新 -->
  <div v-memo="[item.id, item.selected]">
    {{ item.name }}
  </div>
</template>

8.2 shallowRef / shallowReactive

typescript
// 大列表用 shallowRef,避免深层响应式开销
const bigList = shallowRef<Item[]>([])

// 手动触发更新
bigList.value = [...bigList.value, newItem]

8.3 异步组件与懒加载

typescript
import { defineAsyncComponent } from 'vue'

const AsyncComp = defineAsyncComponent(() => {
  return import('./HeavyComponent.vue')
})

// 带选项
const AsyncCompWithOptions = defineAsyncComponent({
  loader: () => import('./HeavyComponent.vue'),
  loadingComponent: LoadingSpinner,
  errorComponent: ErrorDisplay,
  delay: 200,
  timeout: 3000
})

8.4 computed 缓存

typescript
// computed 自动缓存,避免重复计算
const expensive = computed(() => {
  return data.value.map(item => transform(item))
})

// 多处使用同一个 computed,只计算一次

8.5 虚拟列表

vue
<script setup>
import { useVirtualList } from '@vueuse/core'

const { list, containerProps, wrapperProps } = useVirtualList(
  bigArray,
  { itemHeight: 60 }
)
</script>

<template>
  <div v-bind="containerProps" style="height: 400px; overflow: auto">
    <div v-bind="wrapperProps">
      <div v-for="{ data, index } in list" :key="index" style="height: 60px">
        {{ data }}
      </div>
    </div>
  </div>
</template>

九、项目架构最佳实践

9.1 目录结构

src/
├── components/          # 通用组件
│   ├── common/         # 基础组件
│   └── business/       # 业务组件
├── composables/         # 组合式函数
│   ├── useMouse.ts
│   ├── useFetch.ts
│   └── useAuth.ts
├── stores/             # Pinia 状态管理
│   ├── user.ts
│   └── app.ts
├── utils/              # 工具函数
├── api/                # API 请求
├── types/              # 类型定义
├── views/              # 页面
└── App.vue

9.2 Composable 与 Pinia 的关系

特性ComposablePinia Store
状态范围组件内全局
持久化可持久化
DevTools不集成集成
适用场景局部逻辑复用全局状态管理
typescript
// stores/user.ts - 全局状态
import { defineStore } from 'pinia'

export const useUserStore = defineStore('user', () => {
  const user = ref<User | null>(null)
  const token = ref<string>('')
  
  async function login(credentials: Credentials) {
    const res = await api.login(credentials)
    user.value = res.user
    token.value = res.token
  }
  
  return { user, token, login }
})
typescript
// composables/useFormValidation.ts - 局部逻辑复用
export function useFormValidation<T>(initial: T) {
  const errors = ref<Record<string, string>>({})
  const values = reactive({ ...initial })
  
  function validate(rules: ValidationRules) {
    errors.value = {}
    // 验证逻辑
  }
  
  return { values, errors, validate }
}

十、VueUse 常用 Composables

VueUse 是最流行的 Composables 集合:

typescript
import {
  useLocalStorage,    // 本地存储
  useMouse,           // 鼠标位置
  useEventListener,   // 事件监听
  useIntersectionObserver,  // 交叉观察
  useDebounceFn,      // 防抖
  useThrottleFn,      // 节流
  useClipboard,       // 剪贴板
  useDark,            // 暗色模式
  useMediaQuery,      // 媒体查询
  useFullscreen,      // 全屏
  usePermission,      // 权限查询
  useNetwork,         // 网络状态
  useTitle,           // 页面标题
} from '@vueuse/core'

// 暗色模式
const isDark = useDark()
const toggleDark = useToggle(isDark)

// 防抖
const debouncedFn = useDebounceFn(() => {
  console.log('防抖执行')
}, 500)

// 剪贴板
const { text, copy, copied } = useClipboard()

十一、总结

Composition API 是 Vue 3 的核心特性:

  1. <script setup> 是编写组件的标准方式
  2. ref/reactive 是响应式系统的基础
  3. Composables 是逻辑复用的最佳方案
  4. provide/inject 实现跨层级通信
  5. 性能优化 善用 computed 缓存和 shallowRef
  6. VueUse 是不可或缺的工具库

💚 掌握 Composition API,就掌握了 Vue 3 的精髓。


相关文章推荐: