Vue 3 Composition API 实战技巧

别再只会写 setup 了!深入掌握 Composition API 的最佳实践,从 ref/reactive 的选择到高阶 composable 封装,让你的 Vue 代码飞起来。

15 分钟阅读
小明

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 APIComposition API
beforeCreate(setup() 本身就是)
created(setup() 本身就是)
beforeMountonBeforeMount
mountedonMounted
beforeUnmountonBeforeUnmount
unmountedonUnmounted

重点来了:在 setup 里,你不再需要 created 钩子了!任何你想在组件创建时跑的代码,直接写在 setup 函数体里就行了。

<script setup>
console.log('这就在 created 阶段执行了!')

onMounted(() => {
  console.log('DOM 渲染完了!')
})
</script>

六、小明的防坑指南

最后,小明给大家总结几个新手(甚至老手)容易踩的坑:

  1. 解构丢失响应式:这是最大的坑!
    • const { data } = props (props 解构会丢失响应式)
    • const { data } = toRefs(props) 或者直接 props.data
    • (注:Vue 3.5+ 开启响应式 Props 解构实验特性后可以解构,但目前稳妥起见还在观望)
  2. 异步 setup 的陷阱
    • 如果你在 setup 顶层用了 await,组件必须包裹在 <Suspense> 里才能显示。这玩意儿目前还是实验性的。所以,尽量别在 setup 顶层 await,除非你完全掌控了 Suspense。
    • 推荐用我们上面封装的 useAsync 或者 Nuxt 的 useFetch
  3. 不要在循环里通过 index 做 key:这点 Vue 2 就在讲,Vue 3 依然适用。

总结

Composition API 是 Vue 的一次进化,它把"关注点分离"这件事做到了极致。

  • 用 ref 保持心智简单
  • 用 Composable 复用逻辑
  • 用 script setup 简化代码

掌握了这些,你写的就不再是 Vue 代码,而是艺术品(虽然可能是抽象派的)。

好了,今天的课就上到这。如果你觉得有收获,别忘了给小明点个赞。下课!


下期预告:学完了 Composition API,下周我们要去扒一扒 Vue 的原型链,这也是个让无数英雄尽折腰的面试题,敬请期待!