1. 为什么需要组合式 API?
Vue 3 的组合式 API(Composition API)是 Vue 生态中一次重大的范式转变。在传统的选项式 API(Options API)中,我们通过 data、methods、computed 等选项来组织代码。这种方式在小型组件中表现良好,但随着组件复杂度提升,逻辑关注点会分散在不同选项中,导致代码难以维护。
组合式 API 的核心价值在于:
- 更好的逻辑复用:可以将相关逻辑提取到独立的函数中
- 更灵活的组织方式:可以按功能而非选项类型组织代码
- 更好的类型推断:对 TypeScript 支持更友好
- 更小的生产包体积:Tree-shaking 更高效
实际开发中发现,超过 300 行的组件使用选项式 API 维护成本会显著增加,而组合式 API 能保持代码的可读性。
2. 组合式 API 基础
2.1 setup 函数
setup 是组合式 API 的入口函数,它在组件实例创建之前执行。与选项式 API 不同,setup 中无法访问 this,因为此时组件实例尚未创建。
javascript复制import { ref } from 'vue'
export default {
setup() {
const count = ref(0)
function increment() {
count.value++
}
return {
count,
increment
}
}
}
2.2 响应式基础
Vue 3 提供了两种创建响应式数据的主要方式:
- ref:适用于基本类型值
javascript复制const count = ref(0)
console.log(count.value) // 0
count.value++
- reactive:适用于对象
javascript复制const state = reactive({
count: 0,
user: {
name: 'John'
}
})
console.log(state.count) // 0
state.count++
实际项目中,我倾向于对简单数据类型使用 ref,复杂对象使用 reactive。注意 reactive 解构后会失去响应性,这时可以使用 toRefs。
3. 组合式 API 进阶用法
3.1 计算属性和侦听器
计算属性:
javascript复制const doubleCount = computed(() => count.value * 2)
侦听器:
javascript复制watch(count, (newVal, oldVal) => {
console.log(`count changed from ${oldVal} to ${newVal}`)
}, { immediate: true })
深度侦听:
javascript复制watch(
() => state.user,
(newVal) => {
console.log('user changed', newVal)
},
{ deep: true }
)
3.2 生命周期钩子
组合式 API 提供了对应的生命周期钩子函数:
javascript复制import { onMounted, onUpdated, onUnmounted } from 'vue'
setup() {
onMounted(() => {
console.log('组件挂载')
})
onUpdated(() => {
console.log('组件更新')
})
onUnmounted(() => {
console.log('组件卸载')
})
}
注意:组合式 API 的生命周期钩子没有 beforeCreate 和 created,因为 setup 本身就是在这两个阶段之间运行的。
4. 逻辑复用与组合式函数
组合式 API 最强大的特性之一是能够将组件逻辑提取到可复用的函数中:
javascript复制// useCounter.js
import { ref } from 'vue'
export function useCounter(initialValue = 0) {
const count = ref(initialValue)
function increment() {
count.value++
}
function decrement() {
count.value--
}
return {
count,
increment,
decrement
}
}
在组件中使用:
javascript复制import { useCounter } from './useCounter'
export default {
setup() {
const { count, increment } = useCounter(10)
return {
count,
increment
}
}
}
实际项目中,我通常将业务逻辑拆分为多个组合式函数,每个函数只关注单一功能。这种方式显著提高了代码的可维护性和复用性。
5. 与选项式 API 的对比
5.1 代码组织对比
选项式 API:
javascript复制export default {
data() {
return {
count: 0,
searchQuery: ''
}
},
methods: {
increment() {
this.count++
},
search() {
// 搜索逻辑
}
},
computed: {
doubleCount() {
return this.count * 2
}
}
}
组合式 API:
javascript复制export default {
setup() {
const count = ref(0)
const searchQuery = ref('')
const doubleCount = computed(() => count.value * 2)
function increment() {
count.value++
}
function search() {
// 搜索逻辑
}
return {
count,
searchQuery,
doubleCount,
increment,
search
}
}
}
5.2 适用场景
-
选项式 API 更适合:
- 小型项目或简单组件
- 团队中 Vue 新手较多
- 需要快速原型开发
-
组合式 API 更适合:
- 大型复杂应用
- 需要高度逻辑复用的场景
- TypeScript 项目
- 需要更好的代码组织
6. 实战技巧与常见问题
6.1 模板引用
使用 ref 获取 DOM 元素:
javascript复制<template>
<div ref="root">Hello</div>
</template>
<script>
import { ref, onMounted } from 'vue'
export default {
setup() {
const root = ref(null)
onMounted(() => {
console.log(root.value) // <div>Hello</div>
})
return {
root
}
}
}
</script>
6.2 异步组件
javascript复制import { defineAsyncComponent } from 'vue'
const AsyncComponent = defineAsyncComponent(() =>
import('./components/AsyncComponent.vue')
)
export default {
components: {
AsyncComponent
},
setup() {
// ...
}
}
6.3 常见问题
- 响应式丢失问题:
javascript复制// 错误做法
const { count } = reactive({ count: 0 }) // 解构会失去响应性
// 正确做法
const state = reactive({ count: 0 })
const { count } = toRefs(state) // 保持响应性
- setup 中访问 props:
javascript复制export default {
props: ['title'],
setup(props) {
console.log(props.title)
}
}
- 与 Vuex/Pinia 配合使用:
javascript复制import { useStore } from 'vuex'
export default {
setup() {
const store = useStore()
const count = computed(() => store.state.count)
function increment() {
store.commit('increment')
}
return {
count,
increment
}
}
}
7. 性能优化建议
-
合理使用 computed:
- 计算属性会缓存结果,避免不必要的重复计算
- 复杂的计算逻辑应该放在 computed 中而非模板表达式里
-
避免不必要的响应式:
- 不会变化的数据不需要做成响应式
- 大型数组可以考虑使用 shallowRef
-
组件拆分:
- 将大型组件拆分为多个小型组件
- 使用 keep-alive 缓存不常变化的组件
-
懒加载:
- 使用 defineAsyncComponent 懒加载非关键组件
- 路由层面使用懒加载
在实际项目中,我发现组合式 API 配合 Vue Devtools 的性能分析功能,可以更精准地定位性能瓶颈。特别是在大型表格或列表渲染场景下,合理的响应式数据设计能带来显著的性能提升。
8. 组合式 API 生态工具
-
VueUse:
- 提供了大量实用的组合式函数
- 包括浏览器API、传感器、动画等工具
-
Pinia:
- Vue 官方推荐的状态管理库
- 完美支持组合式 API
- 比 Vuex 更简洁的 API
-
VeeValidate:
- 表单验证库
- 提供组合式 API 风格的验证逻辑
-
Vue Router:
- 从 4.x 开始全面支持组合式 API
- 提供了 useRouter 和 useRoute 等组合式函数
9. 从 Vue 2 迁移指南
-
渐进式迁移:
- 可以在同一项目中同时使用选项式 API 和组合式 API
- 通过 @vue/composition-api 插件在 Vue 2 中使用组合式 API
-
主要变化:
- this 不再可用,所有数据都需要通过 setup 返回
- 生命周期钩子名称和用法变化
- 响应式系统完全重写
-
迁移步骤:
-
- 安装 Vue 3 或 @vue/composition-api
-
- 从简单组件开始尝试组合式 API
-
- 逐步重构复杂组件
-
- 迁移全局 API 调用方式
-
在帮助团队迁移的过程中,我发现采用"底部向上"的策略最有效:先迁移工具函数和基础组件,再逐步处理业务组件。同时保持充分的测试覆盖是平稳迁移的关键保障。
10. 最佳实践总结
-
单一职责原则:
- 每个组合式函数只关注一个特定功能
- 保持函数小而专注
-
命名约定:
- 组合式函数以 use 前缀命名(如 useFetch)
- 返回的对象属性命名要有描述性
-
类型安全:
- 为组合式函数提供完整的 TypeScript 类型定义
- 使用泛型增强灵活性
-
文档注释:
- 为每个组合式函数添加 JSDoc 注释
- 说明参数、返回值和用法示例
-
测试策略:
- 组合式函数应该易于单元测试
- 避免依赖组件实例的内部状态
在实际项目架构中,我通常会创建一个 composables 目录来组织所有组合式函数,按照功能领域进行分组。每个组合式函数都配有对应的单元测试和类型定义,这种结构显著提高了代码的可维护性和团队协作效率。
