Vue 3 Composition API 实战技巧
别再只会写 setup 了!深入掌握 Composition API 的最佳实践,从 ref/reactive 的选择到高阶 composable 封装,让你的 Vue 代码飞起来。
Vue 3 Composition API 实战技巧
大家好,我是小明。
还记得上次我们聊 Vue 3 的时候,有不少小伙伴在后台留言:「小明,Composition API 我倒是会用了,所有的代码都往 setup 里一梭子也就完事了。但是怎么感觉代码写出来比 Options API 还乱呢?」
听到这话,小明我就忍不住捂脸了。这就好比给你一把屠龙刀,你却拿来削苹果,不仅浪费,还容易削到手。
Composition API(组合式 API)绝对不是简单的「换个地方写代码」。它的灵魂在于逻辑复用和代码组织。
今天,咱们不聊那些基础语法(什么 ref 是啥,computed 咋写,这些文档里都有),咱们直接上实战技巧。我们要聊的,是那些能让你代码瞬间变优雅、变高级、变"正宗"的玩法。
准备好了吗?系好安全带,我们要发车了!
一、ref vs reactive:世纪之战的终局
这不仅是初学者的问题,很多老手也经常纠结:到底该用 ref 还是 reactive?
1.1 为什么大家都爱 ref?
Vue 官方曾经也很纠结,但现在的社区风向标非常明确:首选 ref。
为什么?想象一下,你有一个变量 count。
如果是 ref,你必须用 count.value 来访问。
如果是 reactive,你可以直接 state.count。
看起来 reactive 更省事对不对?但是!
// reactive 的隐形陷阱
const state = reactive({ count: 0, name: '小明' })
// 💥 结构解构就失去响应式了!
let { count } = state
count++ // state.count 根本不会变
// 💥 重新赋值也会失去响应式!
let otherState = reactive({ count: 1 })
// state = otherState // 这样赋值是完全错误的
而 ref 就像一个诚实的直男:想要值?请找 .value。
虽然写 .value 有点烦(Volar 插件其实能自动补全),但它带来了一个巨大的好处:确定性。当你看到 .value,你知道你在处理一个响应式对象。当你看到一个普通的变量,你知道它就是一个普通值。
1.2 小明的建议
- 默认全用
ref:不管是基本类型(number, string)还是对象、数组。 - 只有在特定场景用
reactive:比如表单数据绑定,或者你需要把一组强相关的状态打包在一起时。
// ✅ 推荐写法
const userInfo = ref({
name: '小明',
age: 18
})
// 修改
userInfo.value.age = 19
二、Composable:这才是杀手锏
如果你用了 Vue 3 还在写这面条一样的代码,那你真的亏大了。Composition API 最大的魔力在于 Composable(组合式函数)。
2.1 什么是 Composable?
简单说,就是把一种有状态的逻辑抽离出来,变成一个函数。这有点像 React Hooks。
举个真实的例子:鼠标位置追踪。
在 Options API 时代,你可能得用 Mixin(这就更噩梦了),或者在组件里写 mounted 监听,unmounted 移除监听。如果好几个组件都要用,你就得复制粘贴。
但在 Vue 3 里:
// 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 }
}
2.2 实战:写一个 "你的异步请求有点东西"
我们在做项目时,发请求是最还没的操作。loading 状态、错误处理、数据重置...如果在每个组件里都写一遍:
// ❌ 笨办法
const loading = ref(false)
const data = ref(null)
const error = ref(null)
async function fetchData() {
loading.value = true
try {
data.value = await api.getData()
} catch (e) {
error.value = e
} finally {
loading.value = false
}
}
太累了!来,小明教你封装一个 useAsync:
// composables/useAsync.ts
import { ref } from 'vue'
export function useAsync<T>(task: () => Promise<T>) {
const loading = ref(false)
const data = ref<T | null>(null)
const error = ref<any>(null)
const execute = async () => {
loading.value = true
error.value = null
try {
data.value = await task()
} catch (e) {
error.value = e
} finally {
loading.value = false
}
}
// 立即执行一次(可选)
// execute()
return { loading, data, error, execute }
}
使用的时候简直不要太爽:
<script setup lang="ts">
import { getUserInfo } from '@/api/user'
import { useAsync } from '@/composables/useAsync'
const { loading, data: user, execute } = useAsync(getUserInfo)
</script>
<template>
<div v-if="loading">加载中...</div>
<div v-else-if="user">你好,{{ user.name }}</div>
<button @click="execute">刷新</button>
</template>
你看,组件代码瞬间干净了,逻辑也得到了完美的复用。这就是 Composition API 的优雅之处。
三、script setup:语法糖还是毒药?
Vue 3.2 推出的 <script setup> 绝对是真香系列。它不是简单的少写几行字,它是心智负担的解药。
3.1 告别 return 地狱
以前写 setup(),那个 return 能把你写吐。定义了 10 个变量,return 里就得写 10 遍。漏写一个,模板里就报错。
// 💀 以前的写法
setup() {
const count = ref(0)
const inc = () => count.value++
// ... 此处省略 100 行代码
return {
count, // 还要记得回来这里注册!
inc
}
}
现在:
<script setup>
const count = ref(0) // 自动暴露给模板,这就完了!
</script>
3.2 更好的 TypeScript 支持
在 <script setup> 里,TypeScript 的类型推断简直如丝般顺滑。defineProps 和 defineEmits 也是宏定义,不需要引入就能用。
<script setup lang="ts">
// 纯类型声明 props,这一刻 Java 程序员流下了羡慕的泪水
defineProps<{
title: string
count?: number
}>()
const emit = defineEmits<{
(e: 'change', id: number): void
(e: 'update', value: string): void
}>()
</script>
四、watch vs watchEffect:谁才是真爱?
这两个也是很多人的迷惑点。
4.1 watch:精确制导导弹
当你需要侦听特定的数据源,并且需要在回调里拿到新值和旧值时,用 watch。它默认是懒执行的(只有数据变了才跑)。
const source = ref(0)
watch(source, (newValue, oldValue) => {
console.log(`变了!从 ${oldValue} 变成了 ${newValue}`)
})
4.2 watchEffect:地毯式轰炸
有些时候,你懒得去列出所有依赖项。只要这块代码里用到的响应式数据变了,我就要重跑一遍。这时候用 watchEffect。它会自动收集依赖,并且默认立即执行一次。
const id = ref(1)
watchEffect(() => {
// 只要 id 变了,这里自动重跑
// 不用像 watch 那样显式写:watch(id, () => ...)
console.log(`正在请求 ID: ${id.value}`)
fetch(`/api/${id.value}`)
})
实战心得:大多数业务逻辑里,watch 更安全可控。watchEffect 适合那些"副作用"很明显,且依赖项很多的场景(比如根据好多筛选条件去发请求)。
五、生命周期的变迁
简单复习一下,Options API 里的生命周期在 Composition API 里都加了 on 前缀:
| Options API | Composition API |
|---|---|
| beforeCreate | (setup() 本身就是) |
| created | (setup() 本身就是) |
| beforeMount | onBeforeMount |
| mounted | onMounted |
| beforeUnmount | onBeforeUnmount |
| unmounted | onUnmounted |
重点来了:在 setup 里,你不再需要 created 钩子了!任何你想在组件创建时跑的代码,直接写在 setup 函数体里就行了。
<script setup>
console.log('这就在 created 阶段执行了!')
onMounted(() => {
console.log('DOM 渲染完了!')
})
</script>
六、小明的防坑指南
最后,小明给大家总结几个新手(甚至老手)容易踩的坑:
- 解构丢失响应式:这是最大的坑!
- ❌
const { data } = props(props 解构会丢失响应式) - ✅
const { data } = toRefs(props)或者直接props.data - (注:Vue 3.5+ 开启响应式 Props 解构实验特性后可以解构,但目前稳妥起见还在观望)
- ❌
- 异步 setup 的陷阱:
- 如果你在
setup顶层用了await,组件必须包裹在<Suspense>里才能显示。这玩意儿目前还是实验性的。所以,尽量别在 setup 顶层 await,除非你完全掌控了 Suspense。 - 推荐用我们上面封装的
useAsync或者 Nuxt 的useFetch。
- 如果你在
- 不要在循环里通过 index 做 key:这点 Vue 2 就在讲,Vue 3 依然适用。
总结
Composition API 是 Vue 的一次进化,它把"关注点分离"这件事做到了极致。
- 用 ref 保持心智简单
- 用 Composable 复用逻辑
- 用 script setup 简化代码
掌握了这些,你写的就不再是 Vue 代码,而是艺术品(虽然可能是抽象派的)。
好了,今天的课就上到这。如果你觉得有收获,别忘了给小明点个赞。下课!
下期预告:学完了 Composition API,下周我们要去扒一扒 Vue 的原型链,这也是个让无数英雄尽折腰的面试题,敬请期待!