Vue3 的 Composition API 是框架自发布以来最重要的特性之一。它不仅解决了 Options API 在复杂组件中的局限性,还为我们提供了更灵活、更可组合的代码组织方式。本文将深入探讨 Composition API 的核心概念、最佳实践以及高级技巧。
为什么需要 Composition API
在 Vue2 的 Options API 中,我们通过 data、methods、computed、watch 等选项来组织代码。随着组件复杂度的增加,这种组织方式暴露出了一些问题:
Options API 的局限性
// 典型的 Options API 代码分散问题
export default {
data() {
return {
user: null,
posts: [],
loading: false,
searchQuery: '',
// ... 更多状态
}
},
computed: {
filteredPosts() { /* ... */ },
userDisplayName() { /* ... */ },
// ... 更多计算属性
},
methods: {
fetchUser() { /* ... */ },
fetchPosts() { /* ... */ },
handleSearch() { /* ... */ },
// ... 更多方法
},
watch: {
searchQuery() { /* ... */ },
user() { /* ... */ },
// ... 更多侦听器
}
}
这种组织方式导致逻辑分散:同一个功能的代码被分散在不同选项中,难以阅读和维护。更糟糕的是,代码复用变得非常困难,只能通过 mixins 和高阶组件,但它们都有各自的问题。
Composition API 的优势
Composition API 让我们可以按照逻辑功能而不是选项类型来组织代码:
// Composition API:按功能组织代码
import { ref, computed, watch, onMounted } from 'vue'
export default {
setup() {
// 用户相关逻辑
const user = ref(null)
const userDisplayName = computed(() => user.value?.name ?? 'Guest')
function fetchUser() {
// 获取用户逻辑
}
// 文章相关逻辑
const posts = ref([])
const searchQuery = ref('')
const filteredPosts = computed(() => {
return posts.value.filter(post =>
post.title.includes(searchQuery.value)
)
})
function fetchPosts() {
// 获取文章逻辑
}
watch(searchQuery, () => {
// 搜索逻辑
})
onMounted(() => {
fetchUser()
fetchPosts()
})
return {
user,
userDisplayName,
posts,
searchQuery,
filteredPosts
}
}
}
"Composition API 不是要替代 Options API,而是提供了另一种选择。对于简单组件,Options API 依然是很好的选择;但对于复杂组件,Composition API 能够显著提升代码的可维护性。"
setup 函数详解
setup 是 Composition API 的入口点。它在组件创建之前执行,是所有 Composition API 函数的使用场所。
基本用法
import { ref, computed } from 'vue'
export default {
setup() {
// 1. 定义响应式状态
const count = ref(0)
// 2. 定义计算属性
const doubled = computed(() => count.value * 2)
// 3. 定义方法
function increment() {
count.value++
}
// 4. 返回给模板使用
return {
count,
doubled,
increment
}
}
}
setup 参数
setup 函数接收两个参数:
export default {
setup(props, context) {
// props: 响应式的 props 对象
console.log(props.message)
// context: 包含三个可选属性
context.attrs // 非响应式的 attrs
context.slots // 非响应式的 slots
context.emit // 触发事件的方法
context.expose // 暴露公共属性(Vue 3.2+)
// 解构写法
const { attrs, slots, emit, expose } = context
return {}
}
}
注意:props 不能解构
// ❌ 错误:会丢失响应性
setup({ title }) {
console.log(title) // 不会更新
}
// ✅ 正确:使用 toRefs
import { toRefs } from 'vue'
setup(props) {
const { title } = toRefs(props)
console.log(title.value) // 会更新
}
返回值与模板访问
// setup 返回对象
export default {
setup() {
return {
message: 'Hello',
count: ref(0)
}
},
// 也可以与其他选项共存
mounted() {
console.log(this.count) // 可以访问
}
}
// setup 返回渲染函数(罕见但强大)
import { h } from 'vue'
export default {
setup() {
return () => h('div', 'Hello World')
}
}
响应式系统核心
Vue3 的响应式系统基于 ES6 Proxy 实现,提供了比 Vue2 的 Object.defineProperty 更强大的能力。
ref vs reactive
这是使用最广泛的两个响应式 API,它们的区别非常重要:
| 特性 | ref | reactive |
|---|---|---|
| 适用类型 | 基本类型、对象 | 仅对象 |
| 访问方式 | .value | 直接访问 |
| 模板解包 | 自动 | 不需要 |
| 重新赋值 | 支持 | 不支持 |
ref 详解
import { ref } from 'vue'
// 基本类型
const count = ref(0)
console.log(count.value) // 0
// 对象类型
const user = ref({ name: 'Alice' })
console.log(user.value.name) // 'Alice'
// 模板中自动解包
// <template>
// <div>{{ count }}</div> // 不需要 .value
// <div>{{ user.name }}</div> // 自动解包
// </template>
// 数组
const items = ref([])
items.value.push({ id: 1 }) // 需要通过 .value
reactive 详解
import { reactive } from 'vue'
// 对象
const state = reactive({
count: 0,
user: { name: 'Alice' }
})
// 访问和修改
state.count++
state.user.name = 'Bob'
// 嵌套对象也是响应式的
// 数组
const list = reactive([{ id: 1 }])
list.push({ id: 2 })
// ⚠️ 注意:不能重新赋值
state = reactive({ count: 1 }) // ❌ 不响应!
// 解决方案:使用 ref 或 Object.assign
toRef 和 toRefs
这两个函数在处理响应式对象时非常有用:
import { reactive, toRef, toRefs } from 'vue'
const state = reactive({
count: 0,
message: 'Hello'
})
// toRef: 创建单个 ref
const countRef = toRef(state, 'count')
countRef.value++ // state.count 也会更新
// toRefs: 将所有属性转为 ref
const { count, message } = toRefs(state)
count.value++
message.value = 'Hi'
// state 也会更新
// 典型用法:在 setup 中返回
setup() {
const state = reactive({
count: 0,
name: 'Vue'
})
return {
...toRefs(state) // 解构时保持响应性
}
}
readonly 和 shallowReadonly
import { reactive, readonly, shallowReadonly } from 'vue'
const original = reactive({ count: 0 })
// 深度只读
const copy = readonly(original)
copy.count++ // ⚠️ 警告:不能修改只读属性
// 浅层只读(深层可修改)
const shallow = shallowReadonly({
nested: { count: 0 }
})
shallow.nested.count++ // ✅ 可以修改深层属性
shallow.other = 'new' // ❌ 不能添加顶层属性
toRaw 和 markRaw
import { reactive, toRaw, markRaw } from 'vue'
const obj = reactive({ count: 0 })
// toRaw: 获取原始对象
const raw = toRaw(obj)
raw.count++ // 不会触发响应式更新
// markRaw: 标记对象永远不需要响应式
const thirdPartyLib = markRaw({
method() {
// 第三方库的方法
}
})
const state = reactive({
lib: thirdPartyLib // 不会转换为响应式
})
计算属性与侦听器
computed 计算属性
import { ref, computed } from 'vue'
const count = ref(0)
// 只读计算属性
const doubled = computed(() => count.value * 2)
// 可写计算属性
const fullName = computed({
get() {
return `${firstName.value} ${lastName.value}`
},
set(newValue) {
[firstName.value, lastName.value] = newValue.split(' ')
}
})
// 计算属性缓存
// computed 会缓存结果,只有依赖变化才重新计算
const expensive = computed(() => {
console.log('computing...')
return heavyCalculation()
})
expensive.value // 'computing...'
expensive.value // 使用缓存,不输出
watch 侦听器
import { ref, watch, watchEffect } from 'vue'
const count = ref(0)
const user = ref({ name: 'Alice' })
// 侦听单个 ref
watch(count, (newValue, oldValue) => {
console.log(`count: ${oldValue} -> ${newValue}`)
})
// 侦听多个源
watch([count, user], ([newCount, newUser], [oldCount, oldUser]) => {
console.log('multiple changed')
})
// 侦听 getter 函数
watch(
() => user.value.name,
(newName, oldName) => {
console.log(`name: ${oldName} -> ${newName}`)
}
)
// 深度侦听
watch(user, (newVal, oldVal) => {
console.log('user changed')
}, { deep: true })
// 立即执行
watch(count, (val) => {
console.log(val)
}, { immediate: true })
watchEffect 自动追踪
import { watchEffect } from 'vue'
const count = ref(0)
const name = ref('Vue')
// 自动追踪依赖
watchEffect(() => {
console.log(`${name.value}: ${count.value}`)
})
// 每当 count 或 name 变化时自动执行
// 停止侦听
const stop = watchEffect(() => {
// ...
})
stop() // 手动停止
// 副作用刷新时机
watchEffect(
() => {
// 副作用代码
},
{
flush: 'post' // 'pre' | 'post' | 'sync'
// pre: 组件更新前(默认)
// post: 组件更新后
// sync: 同步执行
}
)
// onTrack 和 onTrigger 调试
watchEffect(
() => {
console.log(count.value)
},
{
onTrack(e) {
console.log('tracking', e)
},
onTrigger(e) {
console.log('triggered', e)
}
}
)
生命周期钩子
Composition API 使用 onX 格式的生命周期钩子:
| Options API | Composition API |
|---|---|
| beforeCreate | setup() |
| created | setup() |
| beforeMount | onBeforeMount |
| mounted | onMounted |
| beforeUpdate | onBeforeUpdate |
| updated | onUpdated |
| beforeUnmount | onBeforeUnmount |
| unmounted | onUnmounted |
| errorCaptured | onErrorCaptured |
| renderTracked | onRenderTracked |
| renderTriggered | onRenderTriggered |
import {
onMounted,
onUpdated,
onUnmounted,
onBeforeMount
} from 'vue'
export default {
setup() {
onMounted(() => {
console.log('组件已挂载')
})
onBeforeMount(() => {
console.log('即将挂载')
})
onUpdated(() => {
console.log('组件已更新')
})
onUnmounted(() => {
console.log('组件已卸载')
})
// 可以多次调用
onMounted(() => {
console.log('另一个 mounted 钩子')
})
}
}
组合式函数模式
组合式函数(Composables)是复用逻辑的最佳方式。它本质上是一个返回响应式状态的函数:
// composables/useMouse.js
import { ref, onMounted, onUnmounted } from 'vue'
export function useMouse() {
const x = ref(0)
const y = ref(0)
function update(event) {
x.value = event.pageX
y.value = event.pageY
}
onMounted(() => window.addEventListener('mousemove', update))
onUnmounted(() => window.removeEventListener('mousemove', update))
return { x, y }
}
使用组合式函数
import { useMouse } from './composables/useMouse'
export default {
setup() {
const { x, y } = useMouse()
return { x, y }
}
}
// 在 <template> 中
// <div>Mouse: {{ x }}, {{ y }}</div>
更多 Composable 示例
// composables/useFetch.js
import { ref, watchEffect } from 'vue'
export function useFetch(url) {
const data = ref(null)
const error = ref(null)
watchEffect(async () => {
data.value = null
error.value = null
try {
const res = await fetch(url.value)
data.value = await res.json()
} catch (err) {
error.value = err
}
})
return { data, error }
}
// composables/useLocalStorage.js
import { ref, watch } from 'vue'
export function useLocalStorage(key, defaultValue) {
const stored = localStorage.getItem(key)
const value = ref(stored ? JSON.parse(stored) : defaultValue)
watch(value, () => {
localStorage.setItem(key, JSON.stringify(value.value))
}, { deep: true })
return value
}
// 使用
const theme = useLocalStorage('theme', 'light')
const { data, error } = useFetch(ref('/api/users'))
TypeScript 最佳实践
Vue3 + TypeScript 是绝配。Composition API 让类型推断变得更加准确:
import { ref, computed, Ref } from 'vue'
// 定义类型
interface User {
id: number
name: string
email: string
}
// ref 会自动推断类型
const count = ref(0) // Ref<number>
// 也可以显式指定
const user: Ref<User | null> = ref(null)
// reactive 需要类型注解
const state: {
users: User[]
loading: boolean
} = reactive({
users: [],
loading: false
})
// 计算属性类型推断
const userName = computed(() => user.value?.name ?? '')
// Ref<string>
// 组合式函数类型
function useUserApi() {
const users = ref<User[]>([])
const loading = ref(false)
async function fetchUsers(): Promise<void> {
loading.value = true
const res = await fetch('/api/users')
users.value = await res.json()
loading.value = false
}
return {
users,
loading,
fetchUsers
}
}
常见设计模式
1. 状态提取
// store/useTodoStore.js
import { reactive, computed } from 'vue'
const state = reactive({
todos: [],
filter: 'all'
})
export function useTodoStore() {
const filtered = computed(() => {
if (state.filter === 'active') {
return state.todos.filter(t => !t.done)
}
if (state.filter === 'done') {
return state.todos.filter(t => t.done)
}
return state.todos
})
function addTodo(text) {
state.todos.push({
id: Date.now(),
text,
done: false
})
}
return {
state,
filtered,
addTodo
}
}
2. 依赖注入
// 父组件
import { provide, ref } from 'vue'
export default {
setup() {
const theme = ref('dark')
provide('theme', theme)
}
}
// 子组件
import { inject } from 'vue'
export default {
setup() {
const theme = inject('theme', 'light') // 默认值
return { theme }
}
}
3. 异步操作
function useAsync(asyncFn) {
const state = reactive({
data: null,
error: null,
loading: false
})
async function execute(...args) {
state.loading = true
state.error = null
try {
state.data = await asyncFn(...args)
} catch (error) {
state.error = error
} finally {
state.loading = false
}
}
return {
...toRefs(state),
execute
}
}
总结
Vue3 Composition API 提供了一套强大而灵活的工具集,让我们能够更好地组织代码、复用逻辑。掌握好这些核心概念和模式,将帮助你构建更加可维护的应用程序。
关键要点
- 按功能而非选项类型组织代码
- 理解 ref 和 reactive 的区别和使用场景
- 充分利用 Composables 复用逻辑
- 结合 TypeScript 获得更好的类型支持
- 善用 computed 和 watch 管理响应式副作用