Vue 3 Composition API 最佳实践 2026

💚 组合胜于继承。 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 API | Composition API |
|---|---|---|
| 逻辑复用 | mixins(命名冲突) | Composables(清晰) |
| 代码组织 | 按选项分散 | 按功能聚合 |
| 类型推断 | 弱 | 优秀 |
| Tree-shaking | 差 | 好 |
| 代码压缩 | 差 | 好 |
1.2 对比示例
Options API:
<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>):
<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 用于基本类型和需要整体替换的值:
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 用于对象类型,返回深层响应式代理:
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 选择
| 特性 | ref | reactive |
|---|---|---|
| 适用类型 | 任意 | 对象/数组 |
| 访问方式 | .value | 直接 |
| 解构 | 保持响应式 | 失去响应式 |
| 替换 | 直接赋值 | 需要 Object.assign |
| 推荐场景 | 基本类型、需替换 | 表单、复杂对象 |
reactive 解构问题:
const state = reactive({ count: 0, name: 'Vue' })
// 解构后失去响应式!
const { count, name } = state // 不是响应式的
// 使用 toRefs 保持响应式
import { toRefs } from 'vue'
const { count, name } = toRefs(state) // 响应式2.4 其他响应式 API
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
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 最佳实践
// 避免在 computed 中产生副作用
// 错误示例
const bad = computed(() => {
api.fetchData() // 不要在 computed 中做异步操作
return data.value
})
// 正确:用 watch 处理副作用
watch(source, () => {
api.fetchData()
})3.3 watch
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
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
| 特性 | watch | watchEffect |
|---|---|---|
| 依赖追踪 | 显式指定 | 自动追踪 |
| 旧值 | 可获取 | 不提供 |
| 立即执行 | 需 immediate | 默认立即 |
| 适用于 | 需要新旧值对比 | 只需执行副作用 |
四、Composables 组合式函数
4.1 什么是 Composables
Composables 是以 use 开头的函数,封装可复用的有状态逻辑:
// 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 }
}<script setup lang="ts">
import { useCounter } from '@/composables/useCounter'
const { count, double, increment } = useCounter(10)
</script>4.2 实用 Composable:useMouse
// 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
// 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
// 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
}<script setup>
import { useLocalStorage } from '@/composables/useLocalStorage'
const theme = useLocalStorage('theme', 'light')
const settings = useLocalStorage('settings', { sidebar: true, compact: false })
</script>4.5 Composable 设计原则
- 命名规范:以
use开头 - 返回值:返回 ref 或 reactive,不要返回普通值
- 生命周期:在函数内注册
onMounted等 - 输入参数:支持 ref 和普通值
- 无副作用:不在 composable 中直接操作 DOM
- 类型安全:使用泛型
五、依赖注入
5.1 provide / inject
// 父组件
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)// 子组件
import { inject } from 'vue'
const count = inject(countKey) // 类型: Ref<number> | undefined
// 带默认值
const count = inject(countKey, ref(0))5.2 封装 provider
// 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
}<!-- 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>六、生命周期钩子
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
<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+)
<!-- 双向绑定变得简单 -->
<script setup lang="ts">
const model = defineModel<string>()
</script>
<template>
<input v-model="model" />
</template>
<!-- 父组件使用 -->
<!-- <MyInput v-model="text" /> -->7.3 defineExpose
<script setup>
import { ref } from 'vue'
const count = ref(0)
function reset() { count.value = 0 }
// 暴露给父组件通过 ref 访问
defineExpose({ count, reset })
</script><!-- 父组件 -->
<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
<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
<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
// 大列表用 shallowRef,避免深层响应式开销
const bigList = shallowRef<Item[]>([])
// 手动触发更新
bigList.value = [...bigList.value, newItem]8.3 异步组件与懒加载
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 缓存
// computed 自动缓存,避免重复计算
const expensive = computed(() => {
return data.value.map(item => transform(item))
})
// 多处使用同一个 computed,只计算一次8.5 虚拟列表
<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.vue9.2 Composable 与 Pinia 的关系
| 特性 | Composable | Pinia Store |
|---|---|---|
| 状态范围 | 组件内 | 全局 |
| 持久化 | 否 | 可持久化 |
| DevTools | 不集成 | 集成 |
| 适用场景 | 局部逻辑复用 | 全局状态管理 |
// 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 }
})// 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 集合:
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 的核心特性:
<script setup>是编写组件的标准方式- ref/reactive 是响应式系统的基础
- Composables 是逻辑复用的最佳方案
- provide/inject 实现跨层级通信
- 性能优化 善用 computed 缓存和 shallowRef
- VueUse 是不可或缺的工具库
💚 掌握 Composition API,就掌握了 Vue 3 的精髓。
相关文章推荐: