1. 为什么需要组合式 API
Vue 3 的组合式 API(Composition API)彻底改变了我们编写 Vue 组件的方式。作为一名长期使用 Vue 2 的开发者,我最初对这套新 API 也持观望态度,但实际使用后发现它解决了选项式 API 的诸多痛点。
在大型项目中,选项式 API 会导致逻辑关注点分散。比如一个购物车功能,相关的数据(data)、方法(methods)、计算属性(computed)和生命周期钩子(lifecycle hooks)会被拆分到不同选项中,当组件复杂时,理解和维护变得困难。组合式 API 通过逻辑组合解决了这个问题,让相关代码可以组织在一起。
提示:组合式 API 不是用来完全替代选项式 API 的,在简单场景下选项式 API 依然是不错的选择。
组合式 API 的核心优势在于:
- 更好的逻辑复用:可以轻松提取和重用业务逻辑
- 更灵活的代码组织:相关代码可以集中在一起
- 更好的 TypeScript 支持:类型推断更加自然
- 更小的生产包体积:Tree-shaking 更高效
2. 组合式 API 基础概念
2.1 setup() 函数
setup() 是组合式 API 的入口点,它在组件实例创建之前执行。这意味着在 setup() 中无法访问 this,因为组件实例尚未创建。
javascript复制export default {
setup(props, context) {
// 在这里声明响应式状态、计算属性、方法等
// 返回的对象会暴露给模板和其他选项
return {
// 暴露给模板的内容
}
}
}
context 参数包含三个属性:
- attrs:非响应式的组件属性
- slots:插槽内容
- emit:触发事件的方法
2.2 响应式基础
Vue 3 提供了两种创建响应式数据的方式:
- ref():用于基本类型值
javascript复制import { ref } from 'vue'
const count = ref(0)
console.log(count.value) // 0
count.value++
- reactive():用于对象
javascript复制import { reactive } from 'vue'
const state = reactive({
count: 0
})
console.log(state.count) // 0
state.count++
注意:在模板中使用 ref 时不需要 .value,Vue 会自动解包。
2.3 计算属性和侦听器
计算属性使用 computed():
javascript复制import { ref, computed } from 'vue'
const count = ref(0)
const doubleCount = computed(() => count.value * 2)
侦听器使用 watch() 和 watchEffect():
javascript复制import { ref, watch } from 'vue'
const count = ref(0)
watch(count, (newValue, oldValue) => {
console.log(`count changed from ${oldValue} to ${newValue}`)
})
// 立即执行的 watch
watchEffect(() => {
console.log(`count is now ${count.value}`)
})
3. 组合式 API 进阶技巧
3.1 自定义组合函数
组合式 API 最强大的特性之一是能够创建可重用的自定义组合函数。这类似于 React 的自定义 Hook。
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, decrement } = useCounter(0)
return {
count,
increment,
decrement
}
}
}
3.2 生命周期钩子
在组合式 API 中,生命周期钩子是通过特定函数访问的:
javascript复制import { onMounted, onUpdated, onUnmounted } from 'vue'
export default {
setup() {
onMounted(() => {
console.log('组件已挂载')
})
onUpdated(() => {
console.log('组件已更新')
})
onUnmounted(() => {
console.log('组件已卸载')
})
}
}
3.3 模板引用
使用 ref() 创建模板引用:
javascript复制<template>
<input ref="inputRef" />
</template>
<script>
import { ref, onMounted } from 'vue'
export default {
setup() {
const inputRef = ref(null)
onMounted(() => {
inputRef.value.focus()
})
return {
inputRef
}
}
}
</script>
4. 组合式 API 最佳实践
4.1 组织代码结构
良好的代码组织是使用组合式 API 的关键。我推荐以下结构:
- 响应式状态声明
- 计算属性
- 方法
- 生命周期钩子
- 返回模板需要的内容
javascript复制export default {
setup() {
// 1. 响应式状态
const count = ref(0)
const user = reactive({ name: 'John' })
// 2. 计算属性
const doubleCount = computed(() => count.value * 2)
// 3. 方法
function increment() {
count.value++
}
// 4. 生命周期钩子
onMounted(() => {
console.log('组件已挂载')
})
// 5. 返回模板需要的内容
return {
count,
user,
doubleCount,
increment
}
}
}
4.2 类型安全与 TypeScript
组合式 API 天然支持 TypeScript,提供了更好的类型推断:
typescript复制import { ref, computed } from 'vue'
interface User {
name: string
age: number
}
export default {
setup() {
const count = ref<number>(0)
const user = reactive<User>({
name: 'John',
age: 30
})
const greeting = computed<string>(() => `Hello, ${user.name}!`)
return {
count,
user,
greeting
}
}
}
4.3 性能优化
- 避免不必要的响应式:对于不需要响应式的数据,使用普通变量而非 ref/reactive
- 合理使用 computed:对于复杂计算使用 computed 缓存结果
- 谨慎使用 watch:避免深度监听大型对象
- 使用 shallowRef 和 shallowReactive 减少响应式开销
javascript复制import { shallowRef, shallowReactive } from 'vue'
const largeList = shallowRef([]) // 只有 .value 变化会触发更新
const bigObject = shallowReactive({ ... }) // 只跟踪顶层属性变化
5. 实战案例:构建一个任务管理器
让我们用组合式 API 构建一个完整的任务管理器应用。
5.1 项目结构
code复制src/
components/
TaskList.vue
TaskForm.vue
composables/
useTasks.js
App.vue
5.2 useTasks 组合函数
javascript复制// src/composables/useTasks.js
import { ref, computed } from 'vue'
export function useTasks() {
const tasks = ref([])
const filter = ref('all')
const filteredTasks = computed(() => {
switch (filter.value) {
case 'active':
return tasks.value.filter(task => !task.completed)
case 'completed':
return tasks.value.filter(task => task.completed)
default:
return tasks.value
}
})
function addTask(title) {
tasks.value.push({
id: Date.now(),
title,
completed: false
})
}
function toggleTask(id) {
const task = tasks.value.find(t => t.id === id)
if (task) {
task.completed = !task.completed
}
}
function removeTask(id) {
tasks.value = tasks.value.filter(t => t.id !== id)
}
return {
tasks,
filter,
filteredTasks,
addTask,
toggleTask,
removeTask
}
}
5.3 TaskList 组件
javascript复制// src/components/TaskList.vue
<template>
<div>
<div v-for="task in filteredTasks" :key="task.id">
<input
type="checkbox"
:checked="task.completed"
@change="toggleTask(task.id)"
/>
<span :class="{ completed: task.completed }">
{{ task.title }}
</span>
<button @click="removeTask(task.id)">删除</button>
</div>
</div>
</template>
<script>
import { useTasks } from '../composables/useTasks'
export default {
setup() {
const { filteredTasks, toggleTask, removeTask } = useTasks()
return {
filteredTasks,
toggleTask,
removeTask
}
}
}
</script>
5.4 TaskForm 组件
javascript复制// src/components/TaskForm.vue
<template>
<form @submit.prevent="handleSubmit">
<input v-model="newTaskTitle" placeholder="新任务" />
<button type="submit">添加</button>
</form>
</template>
<script>
import { ref } from 'vue'
import { useTasks } from '../composables/useTasks'
export default {
setup() {
const { addTask } = useTasks()
const newTaskTitle = ref('')
function handleSubmit() {
if (newTaskTitle.value.trim()) {
addTask(newTaskTitle.value.trim())
newTaskTitle.value = ''
}
}
return {
newTaskTitle,
handleSubmit
}
}
}
</script>
6. 常见问题与解决方案
6.1 响应式丢失问题
当解构 reactive 对象时,响应式会丢失:
javascript复制const state = reactive({ count: 0 })
const { count } = state // count 不再是响应式的
解决方案:
- 使用 toRefs:
javascript复制const state = reactive({ count: 0 })
const { count } = toRefs(state) // count 现在是 ref
- 直接通过 state 访问:
javascript复制const state = reactive({ count: 0 })
// 在模板中使用 state.count
6.2 异步组件中的 setup
在异步组件中使用 setup 时,需要确保异步操作完成后才渲染组件:
javascript复制import { defineAsyncComponent, ref } from 'vue'
const AsyncComp = defineAsyncComponent({
loader: async () => {
const data = await fetchData()
return {
setup() {
const localData = ref(data)
return { localData }
},
template: '<div>{{ localData }}</div>'
}
}
})
6.3 与 Vuex/Pinia 配合使用
组合式 API 与状态管理库配合良好:
javascript复制import { useStore } from 'vuex'
import { computed } from 'vue'
export default {
setup() {
const store = useStore()
const count = computed(() => store.state.count)
function increment() {
store.commit('increment')
}
return {
count,
increment
}
}
}
对于 Pinia 更是天然适配:
javascript复制import { useCounterStore } from '@/stores/counter'
export default {
setup() {
const counter = useCounterStore()
return {
counter
}
}
}
7. 组合式 API 与选项式 API 对比
7.1 代码组织对比
选项式 API:
javascript复制export default {
data() {
return {
count: 0
}
},
methods: {
increment() {
this.count++
}
},
computed: {
doubleCount() {
return this.count * 2
}
}
}
组合式 API:
javascript复制import { ref, computed } from 'vue'
export default {
setup() {
const count = ref(0)
const doubleCount = computed(() => count.value * 2)
function increment() {
count.value++
}
return {
count,
doubleCount,
increment
}
}
}
7.2 逻辑复用对比
选项式 API 使用 mixins:
javascript复制const counterMixin = {
data() {
return {
count: 0
}
},
methods: {
increment() {
this.count++
}
}
}
export default {
mixins: [counterMixin]
}
组合式 API 使用自定义组合函数:
javascript复制// useCounter.js
import { ref } from 'vue'
export function useCounter() {
const count = ref(0)
function increment() {
count.value++
}
return {
count,
increment
}
}
// 在组件中使用
import { useCounter } from './useCounter'
export default {
setup() {
const { count, increment } = useCounter()
return {
count,
increment
}
}
}
7.3 适用场景
选项式 API 更适合:
- 小型项目
- 简单组件
- 对 Vue 2 熟悉的团队
组合式 API 更适合:
- 大型项目
- 复杂逻辑组件
- 需要更好的 TypeScript 支持
- 需要更好的逻辑复用
8. 组合式 API 生态工具
8.1 VueUse
VueUse 是一个基于组合式 API 的工具集合,提供了大量实用的组合函数:
javascript复制import { useLocalStorage, useMouse } from '@vueuse/core'
export default {
setup() {
const count = useLocalStorage('count', 0)
const { x, y } = useMouse()
return {
count,
x,
y
}
}
}
8.2 Pinia
Pinia 是 Vue 的官方状态管理库,专为组合式 API 设计:
javascript复制// stores/counter.js
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', {
state: () => ({ count: 0 }),
actions: {
increment() {
this.count++
}
}
})
// 在组件中使用
import { useCounterStore } from '@/stores/counter'
export default {
setup() {
const counter = useCounterStore()
return {
counter
}
}
}
8.3 Volar
Volar 是 Vue 3 的官方 VSCode 扩展,提供了对组合式 API 的完美支持:
- 模板类型检查
- 组合函数自动导入
- 更好的代码补全
9. 测试组合式 API 组件
测试组合式 API 组件与测试选项式 API 组件类似,但有一些特殊考虑:
9.1 测试自定义组合函数
javascript复制// useCounter.test.js
import { ref } from 'vue'
import { useCounter } from './useCounter'
test('useCounter', () => {
const { count, increment } = useCounter()
expect(count.value).toBe(0)
increment()
expect(count.value).toBe(1)
})
9.2 测试组件
使用 @vue/test-utils:
javascript复制import { mount } from '@vue/test-utils'
import Counter from './Counter.vue'
test('Counter', async () => {
const wrapper = mount(Counter)
expect(wrapper.text()).toContain('0')
await wrapper.find('button').trigger('click')
expect(wrapper.text()).toContain('1')
})
9.3 测试异步逻辑
javascript复制import { mount, flushPromises } from '@vue/test-utils'
import AsyncComponent from './AsyncComponent.vue'
test('AsyncComponent', async () => {
const wrapper = mount(AsyncComponent)
expect(wrapper.text()).toContain('Loading...')
await flushPromises()
expect(wrapper.text()).toContain('Loaded data')
})
10. 组合式 API 性能优化
10.1 减少响应式开销
- 使用 shallowRef 和 shallowReactive 减少响应式深度
- 对于大型列表,考虑使用虚拟滚动
- 避免在模板中使用复杂表达式,改用 computed
10.2 优化渲染性能
- 使用 v-once 标记静态内容
- 合理使用 v-memo 优化子组件更新
- 拆分大型组件为多个小组件
10.3 懒加载组件
使用 defineAsyncComponent 懒加载组件:
javascript复制import { defineAsyncComponent } from 'vue'
const HeavyComponent = defineAsyncComponent(() =>
import('./HeavyComponent.vue')
)
export default {
components: {
HeavyComponent
}
}
11. 组合式 API 与 SSR
组合式 API 完全支持服务端渲染(SSR),但需要注意:
11.1 避免共享状态
确保每个请求都有独立的状态:
javascript复制// 错误:共享状态
const globalState = reactive({ count: 0 })
// 正确:每个请求创建新状态
export function createApp() {
const appState = reactive({ count: 0 })
const app = createSSRApp({
setup() {
return { appState }
}
})
return app
}
11.2 处理异步数据
使用 async setup 或 Suspense:
javascript复制export default {
async setup() {
const data = await fetchData()
return {
data
}
}
}
12. 组合式 API 的未来
Vue 团队正在开发更多基于组合式 API 的特性:
- Reactivity Transform:简化 ref 的使用,不再需要 .value
- Better TypeScript 支持:更精确的类型推断
- 更多官方组合函数:如 useRouter、useRoute 等
组合式 API 代表了 Vue 的未来发展方向,掌握它将使你保持在 Vue 开发的前沿。
