Vue 3 的组合式 API(Composition API)是对组件逻辑组织方式的一次根本性革新。相比选项式 API 按选项类型分散代码的方式,组合式 API 允许开发者按功能关注点组织代码,使逻辑复用更加灵活。本文将系统介绍组合式 API 的核心概念和实际项目中的最佳实践。
一、setup() 与 <script setup>
setup() 是组合式 API 的入口函数,在组件创建之前执行。在实际项目中,推荐使用 <script setup> 语法糖,它让代码更简洁,且编译时自动处理了返回值暴露。
<!-- 推荐:使用 script setup -->
<script setup>
import { ref, computed, onMounted } from 'vue'
const count = ref(0)
const doubled = computed(() => count.value * 2)
function increment() {
count.value++
}
onMounted(() => {
console.log('组件已挂载,初始值:', count.value)
})
</script>
<template>
<p>计数: {{ count }},双倍: {{ doubled }}</p>
<button @click="increment">+1</button>
</template>
二、ref 与 reactive 的选择
ref 和 reactive 是创建响应式数据的两种方式。理解它们的区别是正确使用组合式 API 的基础。
import { ref, reactive } from 'vue'
// ref:适用于基本类型和需要重新赋值的场景
const title = ref('Hello')
const items = ref([]) // 可以整体替换:items.value = newList
// reactive:适用于复杂的对象结构
const form = reactive({
username: '',
password: '',
remember: false,
errors: {}
})
// 修改 reactive 属性直接访问,不需要 .value
form.username = 'admin'
form.errors.username = '用户名不能为空'
核心原则:优先使用 ref。ref 支持任意类型的重新赋值,在解构时不会丢失响应性(配合 toRefs),并且是组合函数返回值的标准选择。reactive 适合管理紧密关联的对象状态,但它不能替换整个对象,解构后会丢失响应性。
三、computed、watch 与 watchEffect
import { ref, computed, watch, watchEffect } from 'vue'
const keyword = ref('')
const items = ref(['Apple', 'Banana', 'Cherry', 'Date'])
// computed:声明式派生状态
const filtered = computed(() =>
items.value.filter(item =>
item.toLowerCase().includes(keyword.value.toLowerCase())
)
)
// watch:精确侦听特定源,可访问新值和旧值
watch(keyword, (newVal, oldVal) => {
console.log(`搜索关键词从 "${oldVal}" 变为 "${newVal}"`)
}, { immediate: false, deep: false })
// watchEffect:自动追踪依赖,立即执行
watchEffect(() => {
console.log(`当前筛选结果共 ${filtered.value.length} 条`)
// 自动追踪 filtered 这个依赖
})
使用建议:需要缓存计算结果时用 computed;需要对比新旧值或控制执行时机时用 watch;需要自动收集依赖并立即执行副作用时用 watchEffect。
四、Composables:逻辑复用的核心模式
Composables 是组合式 API 最强大的能力之一。通过将相关逻辑提取为独立函数,可以在不同组件间轻松复用,彻底替代了 Vue 2 中 Mixins 的方案。
// composables/useFetch.js
import { ref, shallowRef, isShallow } from 'vue'
export function useFetch(url) {
const data = shallowRef(null)
const error = ref(null)
const loading = ref(false)
async function execute(reqUrl = url) {
loading.value = true
error.value = null
try {
const res = await fetch(reqUrl)
if (!res.ok) throw new Error(`HTTP ${res.status}`)
data.value = await res.json()
} catch (err) {
error.value = err.message
} finally {
loading.value = false
}
}
execute()
return { data, error, loading, execute }
}
// 在组件中使用
// <script setup>
// import { useFetch } from '@/composables/useFetch'
// const { data: users, loading, error } = useFetch('/api/users')
// </script>
Composables 的命名约定是以 use 开头,返回值使用 ref 而非 reactive,这样调用方可以灵活解构。
五、生命周期钩子
在组合式 API 中,生命周期钩子变成了以 on 为前缀的函数,可以在 setup 中多次调用。
import {
onMounted,
onUpdated,
onUnmounted,
onBeforeMount,
onBeforeUnmount
} from 'vue'
onMounted(() => {
// DOM 已挂载,可以访问 refs
console.log('组件挂载完成')
})
onUnmounted(() => {
// 清理定时器、事件监听、WebSocket 连接等
clearInterval(timer)
window.removeEventListener('resize', handleResize)
})
六、provide / inject 深层依赖注入
当组件层级较深时,逐层传递 props 非常繁琐。provide / inject 允许祖先组件向所有后代组件注入数据,跨越中间层级。
// 祖先组件
import { provide, ref, readonly } from 'vue'
const theme = ref('dark')
const user = ref({ name: '张三', role: 'admin' })
// 推荐使用 readonly 防止子组件意外修改
provide('theme', readonly(theme))
provide('user', readonly(user))
provide('updateTheme', (val) => { theme.value = val })
// 后代组件
import { inject } from 'vue'
const theme = inject('theme', 'light') // 第二个参数是默认值
const user = inject('user')
const updateTheme = inject('updateTheme')
七、常见陷阱与最佳实践
1. 避免在 reactive 对象上直接解构。解构会丢失响应性,需要使用 toRefs 包装:
import { reactive, toRefs } from 'vue'
const state = reactive({ x: 0, y: 0 })
// const { x, y } = state // 错误:丢失响应性
const { x, y } = toRefs(state) // 正确:x 和 y 仍然是 ref
2. 在异步回调中正确使用响应式数据。ref 和 reactive 的响应式绑定是永久的,在 setTimeout、Promise.then 等异步回调中可以直接访问,无需额外处理。
3. 合理使用 shallowRef 和 shallowReactive。当数据层级很深且不需要深层响应性时,浅层响应式可以避免不必要的代理开销,提升性能。
4. watch 中避免侦听 reactive 对象的属性。正确的做法是使用 getter 函数:
const state = reactive({ count: 0 })
// 错误:watch(state.count, ...) 只获取了初始值
// 正确:
watch(() => state.count, (newVal) => {
console.log('count 变为:', newVal)
})
总结
组合式 API 的本质是将组件逻辑从"按选项类型组织"转变为"按功能关注点组织"。掌握 ref/reactive 的选择策略、watch 的使用场景、composables 的提取方法,是写好 Vue 3 代码的关键。在实际项目中,坚持 composables 的命名和返回值规范,善用 <script setup> 语法糖,可以让代码更加清晰、可维护且易于复用。