1. 为什么我们需要组合式 API?
三年前我刚接手一个大型 Vue 2 项目时,遇到了一个典型的组件问题:一个订单详情组件里混入了 5 个不同的 mixins,每个 mixins 都贡献了自己的 data、methods 和生命周期钩子。当出现一个诡异的 bug 时,我花了整整两天时间才理清楚各个 mixins 之间的交互关系。这种经历让我深刻理解了 Vue 3 引入组合式 API 的必要性。
组合式 API 的核心设计理念是将相关逻辑组织在一起,而不是按照选项(data、methods 等)的类型来分割代码。想象一下你的代码是一本书,选项式 API 就像把所有的名词放在第一章,动词放在第二章,形容词放在第三章;而组合式 API 则是按照故事情节来组织内容,让相关的名词、动词和形容词都出现在同一个场景里。
重要提示:组合式 API 不是要完全取代选项式 API,在简单场景下选项式 API 仍然是最直观的选择。但当组件逻辑变得复杂时,组合式 API 的优势就会非常明显。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 从 Mixins 到组合式函数:一次思维转变
2.1 Mixins 的三大痛点
在我使用 Mixins 的三年经验中,总结出了三个最致命的问题:
-
命名冲突:当多个 Mixins 定义了相同的属性或方法时,后引入的会覆盖前面的,这种隐式的覆盖关系很难追踪。我曾经遇到过一个 bug,两个不同的 Mixins 都定义了
handleSubmit方法,导致表单提交逻辑完全错乱。 -
隐式依赖:Mixins 通常假设组件中存在某些特定的属性或方法,这种隐式依赖使得代码难以理解和维护。有一次我修改了一个基础组件的结构,结果导致 12 个使用相关 Mixins 的组件全部报错。
-
全局状态污染:Mixins 可以修改组件的任何部分,这种无限制的访问权限使得状态变化难以追踪。在一个电商项目中,我们花了三周时间才定位到一个由 Mixins 引起的全局状态污染问题。
2.2 组合式函数的优势对比
组合式函数通过显式的导入和返回值,完美解决了上述问题:
javascript复制// 使用组合式函数
import { useUser } from '@/composables/useUser'
export default {
setup() {
const { user, fetchUser } = useUser()
return {
user,
fetchUser
}
}
}
与 Mixins 相比,组合式函数具有以下优势:
| 特性 | Mixins | 组合式函数 |
|---|---|---|
| 命名冲突 | 隐式合并,容易冲突 | 显式命名,可自由重命名 |
| 代码组织 | 按选项类型分散 | 按逻辑功能集中 |
| 依赖关系 | 隐式依赖组件结构 | 显式声明依赖 |
| 类型支持 | 困难 | 完善的 TypeScript 支持 |
| 可复用性 | 组件级别 | 函数级别,更细粒度 |
3. 组合式 API 核心概念深度解析
3.1 setup() 函数:新的组件入口
setup() 函数是组合式 API 的入口点,它在组件实例创建之前执行。这意味着在 setup() 内部:
- 没有
this上下文(这是有意为之的设计) - 不能访问组件选项(data、methods 等)
- 可以返回对象(将作为模板的渲染上下文)或返回渲染函数
一个典型的 setup() 使用模式:
javascript复制import { ref, onMounted } from 'vue'
export default {
setup(props, context) {
const count = ref(0)
const increment = () => {
count.value++
}
onMounted(() => {
console.log('组件挂载完成')
})
return {
count,
increment
}
}
}
实践经验:在大型项目中,我建议将复杂的
setup()逻辑拆分为多个组合式函数,每个函数负责一个特定的功能领域。
3.2 响应式基础:ref 和 reactive 的选择
Vue 3 提供了两种创建响应式数据的方式:
-
ref:适用于基本类型和对象引用
javascript复制const count = ref(0) // 访问时需要 .value const user = ref({ name: 'John' }) // user.value.name -
reactive:仅适用于对象
javascript复制const state = reactive({ count: 0, user: { name: 'John' } }) // 直接访问 state.count
选择建议:
- 当需要保持对基本类型值的响应性时,必须使用
ref - 当处理一个逻辑上相关联的对象时,使用
reactive更自然 - 在组合式函数中返回时,通常使用
ref以便于解构
3.3 生命周期钩子的新写法
在组合式 API 中,生命周期钩子是通过导入的函数来使用的:
javascript复制import { onMounted, onUpdated, onUnmounted } from 'vue'
setup() {
onMounted(() => {
console.log('mounted!')
})
onUpdated(() => {
console.log('updated!')
})
onUnmounted(() => {
console.log('unmounted!')
})
}
与选项式 API 的生命周期钩子对比:
| 选项式 API | 组合式 API |
|---|---|
| beforeCreate | 不需要(setup 替代) |
| created | 不需要(setup 替代) |
| beforeMount | onBeforeMount |
| mounted | onMounted |
| beforeUpdate | onBeforeUpdate |
| updated | onUpdated |
| beforeUnmount | onBeforeUnmount |
| unmounted | onUnmounted |
4. 构建可复用的组合式函数
4.1 设计原则:单一职责与明确依赖
一个好的组合式函数应该遵循以下原则:
-
单一职责:每个组合式函数只解决一个特定问题。比如
useMousePosition只跟踪鼠标位置,useFetch只处理数据获取。 -
明确依赖:所有依赖都应该作为参数传入。避免直接访问外部状态或调用外部方法。
-
清晰接口:通过返回对象暴露有限的、文档化的 API。
示例:一个获取用户数据的组合式函数
javascript复制// composables/useUser.js
import { ref, onMounted } from 'vue'
import api from '@/api'
export function useUser(userId) {
const user = ref(null)
const loading = ref(false)
const error = ref(null)
const fetchUser = async () => {
try {
loading.value = true
user.value = await api.getUser(userId.value)
} catch (err) {
error.value = err
} finally {
loading.value = false
}
}
onMounted(fetchUser)
return {
user,
loading,
error,
refresh: fetchUser
}
}
4.2 实际案例:实现一个防抖搜索
下面是一个我在实际项目中使用的防抖搜索组合式函数:
javascript复制// composables/useDebouncedSearch.js
import { ref, watch } from 'vue'
export function useDebouncedSearch(searchFn, delay = 300) {
const searchQuery = ref('')
const results = ref([])
const isLoading = ref(false)
let timeoutId = null
watch(searchQuery, (newQuery) => {
clearTimeout(timeoutId)
if (!newQuery.trim()) {
results.value = []
return
}
isLoading.value = true
timeoutId = setTimeout(async () => {
try {
results.value = await searchFn(newQuery)
} finally {
isLoading.value = false
}
}, delay)
})
return {
searchQuery,
results,
isLoading
}
}
使用示例:
javascript复制import { useDebouncedSearch } from '@/composables/useDebouncedSearch'
export default {
setup() {
const searchUsers = async (query) => {
const response = await fetch(`/api/users?q=${query}`)
return response.json()
}
const {
searchQuery,
results,
isLoading
} = useDebouncedSearch(searchUsers)
return {
searchQuery,
users: results,
isLoading
}
}
}
4.3 组合式函数的测试策略
组合式函数的一个巨大优势是它们很容易测试。以下是如何测试上面的防抖搜索函数:
javascript复制import { useDebouncedSearch } from './useDebouncedSearch'
import { nextTick, ref } from 'vue'
describe('useDebouncedSearch', () => {
jest.useFakeTimers()
it('should debounce search calls', async () => {
const mockSearch = jest.fn().mockResolvedValue(['result'])
const { searchQuery, results } = useDebouncedSearch(mockSearch, 300)
searchQuery.value = 'test'
await nextTick()
expect(mockSearch).not.toHaveBeenCalled()
jest.advanceTimersByTime(300)
await nextTick()
expect(mockSearch).toHaveBeenCalledWith('test')
expect(results.value).toEqual(['result'])
})
})
5. 组合式 API 进阶模式
5.1 依赖注入:provide/inject 的现代化用法
在大型应用中,组合式 API 与 provide/inject 结合可以创建灵活的依赖注入系统:
javascript复制// 提供者组件
import { provide, ref } from 'vue'
export default {
setup() {
const theme = ref('dark')
provide('theme', {
theme,
toggleTheme: () => {
theme.value = theme.value === 'dark' ? 'light' : 'dark'
}
})
}
}
// 消费者组件
import { inject } from 'vue'
export default {
setup() {
const themeContext = inject('theme', {
theme: 'light',
toggleTheme: () => {}
})
return {
themeContext
}
}
}
5.2 使用 Suspense 处理异步依赖
组合式 API 与 Suspense 结合可以优雅地处理异步依赖:
javascript复制// 异步组件
import { ref } from 'vue'
export default {
async setup() {
const data = ref(null)
data.value = await fetchData()
return {
data
}
}
}
// 父组件中使用 Suspense
<template>
<Suspense>
<template #default>
<AsyncComponent />
</template>
<template #fallback>
<div>Loading...</div>
</template>
</Suspense>
</template>
5.3 与 TypeScript 的深度集成
组合式 API 天生对 TypeScript 友好。以下是一个类型安全的组合式函数示例:
typescript复制import { ref, Ref } from 'vue'
interface User {
id: number
name: string
email: string
}
interface UseUserReturn {
user: Ref<User | null>
loading: Ref<boolean>
error: Ref<Error | null>
fetchUser: () => Promise<void>
}
export function useUser(userId: Ref<number>): UseUserReturn {
const user = ref<User | null>(null)
const loading = ref(false)
const error = ref<Error | null>(null)
const fetchUser = async () => {
try {
loading.value = true
const response = await fetch(`/api/users/${userId.value}`)
user.value = await response.json()
} catch (err) {
error.value = err as Error
} finally {
loading.value = false
}
}
return {
user,
loading,
error,
fetchUser
}
}
6. 迁移策略:从 Mixins 到组合式函数
6.1 逐步迁移的最佳实践
根据我的迁移经验,推荐以下步骤:
-
识别高价值候选:优先迁移那些被多个组件使用的、逻辑独立的 Mixins。
-
创建组合式函数:将 Mixins 的逻辑重写为组合式函数,保持接口尽可能相似。
-
并行运行:在新组件中使用组合式函数,同时保持旧 Mixins 可用。
-
逐步替换:逐个组件迁移,每次迁移后充分测试。
-
最终清理:当所有使用该 Mixins 的组件都迁移完成后,删除 Mixins。
6.2 常见问题与解决方案
问题1:Mixins 依赖于组件选项
解决方案:将这些依赖作为参数传递给组合式函数。
javascript复制// 之前
export const formMixin = {
methods: {
validate() {
return this.$refs.form.validate()
}
}
}
// 之后
export function useForm(formRef) {
const validate = () => {
return formRef.value.validate()
}
return { validate }
}
问题2:Mixins 之间有依赖关系
解决方案:将依赖的 Mixins 转换为组合式函数,然后在新的组合式函数中组合它们。
javascript复制// 之前
export const authMixin = {
// ...
}
export const userMixin = {
mixins: [authMixin],
// ...
}
// 之后
export function useAuth() {
// ...
}
export function useUser() {
const auth = useAuth()
// 使用 auth
// ...
}
7. 性能优化与最佳实践
7.1 减少响应式开销
组合式 API 的灵活性也意味着需要更注意性能:
-
避免不必要的响应式:不是所有的数据都需要是响应式的。对于不会变化的数据,使用普通变量即可。
-
合理使用 shallowRef 和 shallowReactive:当不需要深度响应时,这些浅层 API 可以减少性能开销。
javascript复制import { shallowRef, shallowReactive } from 'vue'
const largeList = shallowRef([]) // 只有 .value 替换是响应式的
const config = shallowReactive({ // 只有根级别属性是响应式的
apiUrl: '...',
timeout: 1000
})
7.2 内存管理注意事项
组合式函数可能会创建需要清理的副作用(如事件监听器、定时器等)。确保使用 onUnmounted 进行清理:
javascript复制import { onUnmounted } from 'vue'
export function useEventListener(target, event, callback) {
target.addEventListener(event, callback)
onUnmounted(() => {
target.removeEventListener(event, callback)
})
}
7.3 组合式函数的组织建议
在大型项目中,我推荐以下目录结构:
code复制src/
composables/
useUser.js # 用户相关逻辑
useForm.js # 表单处理
useApi.js # API 调用
useRouter.js # 路由相关
index.js # 统一导出
在 index.js 中统一导出所有组合式函数:
javascript复制export * from './useUser'
export * from './useForm'
export * from './useApi'
export * from './useRouter'
这样使用时可以统一导入:
javascript复制import { useUser, useForm } from '@/composables'
8. 实战:构建一个完整的组合式应用
让我们构建一个简单的任务管理应用,展示多个组合式函数的协作:
javascript复制// composables/useTasks.js
import { ref, computed } from 'vue'
import { useApi } from './useApi'
export function useTasks() {
const { get, post, put, del } = useApi('/tasks')
const tasks = ref([])
const loading = ref(false)
const fetchTasks = async () => {
loading.value = true
tasks.value = await get()
loading.value = false
}
const addTask = async (title) => {
const newTask = await post({ title, completed: false })
tasks.value.push(newTask)
}
const toggleTask = async (id) => {
const task = tasks.value.find(t => t.id === id)
if (task) {
const updated = await put(id, {
...task,
completed: !task.completed
})
Object.assign(task, updated)
}
}
const deleteTask = async (id) => {
await del(id)
tasks.value = tasks.value.filter(t => t.id !== id)
}
const completedTasks = computed(() =>
tasks.value.filter(t => t.completed)
)
const pendingTasks = computed(() =>
tasks.value.filter(t => !t.completed)
)
return {
tasks,
loading,
fetchTasks,
addTask,
toggleTask,
deleteTask,
completedTasks,
pendingTasks
}
}
在组件中使用:
javascript复制import { useTasks } from '@/composables/useTasks'
import { onMounted } from 'vue'
export default {
setup() {
const {
tasks,
loading,
fetchTasks,
addTask,
toggleTask,
deleteTask,
completedTasks,
pendingTasks
} = useTasks()
const newTaskTitle = ref('')
const handleAddTask = () => {
if (newTaskTitle.value.trim()) {
addTask(newTaskTitle.value)
newTaskTitle.value = ''
}
}
onMounted(fetchTasks)
return {
tasks,
loading,
newTaskTitle,
handleAddTask,
toggleTask,
deleteTask,
completedTasks,
pendingTasks
}
}
}
这个例子展示了如何将数据获取、状态管理和业务逻辑完全封装在组合式函数中,使组件保持简洁和专注。
