Vue 3 Composition API 实战指南
为什么需要 Composition API?
Vue 2 的 Options API(选项式 API)在组件逻辑复杂时,会面临一个经典问题——逻辑分散。同一个功能的代码被分散在 data、methods、computed、watch 等不同选项中,导致代码难以维护和复用。
Composition API(组合式 API)通过一组全新的 API,让我们可以按逻辑关注点组织代码,而不是按选项类型。
<script setup>
// Options API:同一功能的代码分散在各处
export default {
data() {
return { count: 0, doubled: 0 }
},
methods: {
increment() { this.count++ }
},
watch: {
count(val) { this.doubled = val * 2 }
}
}
</script>
<script setup>
// Composition API:按逻辑组织
const count = ref(0)
const doubled = computed(() => count.value * 2)
function increment() { count.value++ }
</script>核心 API
ref 与 reactive
ref 和 reactive 是 Vue 3 中创建响应式数据的两个核心 API:
<script setup>
import { ref, reactive } from 'vue'
// ref:适用于基本类型和需要重新赋值的场景
const count = ref(0)
const message = ref('hello')
// reactive:适用于对象类型
const user = reactive({
name: 'Alice',
age: 25,
hobbies: ['coding', 'reading']
})
// 修改数据
function updateUser() {
count.value++ // ref 需要通过 .value 访问
user.age = 26 // reactive 直接访问
}
</script>
<template>
<p>{{ count }}</p>
<p>{{ user.name }} - {{ user.age }}</p>
</template>computed
计算属性用于派生状态:
<script setup>
import { ref, computed } from 'vue'
const todos = ref([
{ text: '学习 Vue 3', done: true },
{ text: '写博客', done: false },
{ text: '锻炼身体', done: false }
])
// 只读计算属性
const completedCount = computed(() =>
todos.value.filter(t => t.done).length
)
// 可写计算属性
const allDone = computed({
get() {
return todos.value.every(t => t.done)
},
set(value) {
todos.value.forEach(t => { t.done = value })
}
})
</script>watch 与 watchEffect
侦听器用于响应数据变化执行副作用:
<script setup>
import { ref, watch, watchEffect } from 'vue'
const searchQuery = ref('')
const results = ref([])
// watch:监听特定数据源
watch(searchQuery, async (newVal, oldVal) => {
if (newVal.length > 2) {
results.value = await fetchSearch(newVal)
}
}, { debounce: 300 })
// watchEffect:自动追踪依赖
watchEffect(() => {
// 会自动追踪内部使用的响应式数据
console.log(`当前搜索: ${searchQuery.value}`)
document.title = `搜索: ${searchQuery.value}`
})
</script>逻辑复用:Composables
Composition API 最大的优势之一就是逻辑复用。我们可以将可复用的逻辑提取为 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 }
}在组件中使用:
<script setup>
import { useMouse } from './composables/useMouse'
const { x, y } = useMouse()
</script>
<template>
<p>鼠标位置: {{ x }}, {{ y }}</p>
</template>示例:useLocalStorage
// composables/useLocalStorage.ts
import { ref, watch } from 'vue'
export function useLocalStorage<T>(key: string, defaultValue: T) {
const stored = localStorage.getItem(key)
const data = ref<T>(stored ? JSON.parse(stored) : defaultValue)
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', {
fontSize: 14,
language: 'zh-CN'
})
function toggleTheme() {
theme.value = theme.value === 'light' ? 'dark' : 'light'
}
</script>实战:Todo List
结合以上知识,实现一个完整的 Todo List 组件:
<script setup>
import { ref, computed } from 'vue'
// 状态
const todos = ref([
{ id: 1, text: '学习 Composition API', done: true },
{ id: 2, text: '写一个 composable', done: false },
])
const newTodo = ref('')
const filter = ref('all')
// 计算属性
const filteredTodos = computed(() => {
switch (filter.value) {
case 'active': return todos.value.filter(t => !t.done)
case 'completed': return todos.value.filter(t => t.done)
default: return todos.value
}
})
const activeCount = computed(() =>
todos.value.filter(t => !t.done).length
)
// 方法
function addTodo() {
if (!newTodo.value.trim()) return
todos.value.push({
id: Date.now(),
text: newTodo.value.trim(),
done: false
})
newTodo.value = ''
}
function toggleTodo(id: number) {
const todo = todos.value.find(t => t.id === id)
if (todo) todo.done = !todo.done
}
function removeTodo(id: number) {
todos.value = todos.value.filter(t => t.id !== id)
}
</script>
<template>
<div class="todo-app">
<h2>Todo List ({{ activeCount }} 项未完成)</h2>
<input
v-model="newTodo"
@keyup.enter="addTodo"
placeholder="添加新任务..."
/>
<div class="filters">
<button @click="filter = 'all'">全部</button>
<button @click="filter = 'active'">进行中</button>
<button @click="filter = 'completed'">已完成</button>
</div>
<ul>
<li v-for="todo in filteredTodos" :key="todo.id">
<input
type="checkbox"
:checked="todo.done"
@change="toggleTodo(todo.id)"
/>
<span :class="{ done: todo.done }">{{ todo.text }}</span>
<button @click="removeTodo(todo.id)">删除</button>
</li>
</ul>
</div>
</template>生命周期钩子
Composition API 中的生命周期钩子:
<script setup>
import {
onMounted,
onUnmounted,
onBeforeMount,
onUpdated,
onBeforeUpdate,
onActivated,
onDeactivated
} from 'vue'
onBeforeMount(() => {
console.log('组件挂载前')
})
onMounted(() => {
console.log('组件已挂载')
// 适合:API 请求、DOM 操作、事件监听
})
onBeforeUpdate(() => {
console.log('组件更新前')
})
onUpdated(() => {
console.log('组件已更新')
})
onUnmounted(() => {
console.log('组件已卸载')
// 适合:清理定时器、移除事件监听
})
</script>最佳实践
1. 使用 <script setup>
<script setup> 是 Composition API 的语法糖,推荐在所有新项目中使用:
<script setup>
// 无需 export default
// 顶层的绑定会自动暴露给模板
import { ref } from 'vue'
const count = ref(0)
</script>
<template>
<button @click="count++">{{ count }}</button>
</template>2. 合理拆分 composable
- 一个 composable 只做一件事
- 使用明确的命名约定(useXxx)
- 返回对象而不是直接修改外部状态
3. 类型安全
结合 TypeScript 获得更好的类型推导:
interface User {
id: number
name: string
email: string
}
export function useUser(id: Ref<number>) {
const user = ref<User | null>(null)
const loading = ref(false)
watch(id, async (newId) => {
loading.value = true
user.value = await fetchUser(newId)
loading.value = false
}, { immediate: true })
return { user, loading }
}总结
Composition API 不是要取代 Options API,而是提供了一种更灵活、更强大的代码组织方式。它特别适合:
- 复杂组件:逻辑按功能聚合,而非按选项分散
- 逻辑复用:通过 composable 轻松共享状态逻辑
- 类型推导:与 TypeScript 配合得天衣无缝
- Tree-shaking:按需导入,减小打包体积
从 Options API 到 Composition API 的转变,不仅是 API 的变化,更是一种思维方式的升级——从”我的数据在哪里”到”我的功能在哪里”。