组合式 API 是 Vue 3 引入的一套全新的逻辑组织和代码复用方式。它允许你在同一个作用域内集中处理某一功能的响应式数据、计算属性、侦听器、生命周期钩子等,而不是像选项式 API 那样按配置项分散到 data、methods、computed、watch 等不同区域。
最直观的好处是:当一个组件的功能变复杂时,相关逻辑可以天然地聚合在一起,不用在多个选项块之间来回跳转。
两种写法:setup() 函数与 <script setup> 语法糖
组合式 API 有两种书写方式。传统方式是在组件选项中定义一个 setup() 函数,所有组合式 API 都在这个函数内部调用,并返回模板需要的数据和方法:
<script>
import { ref, computed } from 'vue'
export default {
setup() {
const count = ref(0)
const double = computed(() => count.value * 2)
function increment() {
count.value++
}
// 模板中需要用到的变量/方法,必须在这里显式返回
return { count, double, increment }
}
}
</script>
而 <script setup> 是官方推荐的语法糖,它消隐了 setup() 函数和 return 语句,让代码更简洁:
<script setup>
import { ref, computed } from 'vue'
const count = ref(0)
const double = computed(() => count.value * 2)
function increment() {
count.value++
}
// 无需 return,顶层绑定会自动暴露给模板
</script>
两者完全等价,但 <script setup> 减少了样板代码,同时配合 TypeScript 和 IDE 支持更流畅。在实际项目中,几乎都选择 <script setup> 作为默认写法。唯一的制约是:<script setup> 中不能再使用 export default 单独导出其他内容(如组件级选项),需要用专门的编译器宏(如 defineProps、defineEmits、defineExpose)完成。
核心响应式 API
组合式 API 中常用的响应式工具主要有以下几个:
1. ref
用于包装任意类型的值(基本类型或对象),使其成为响应式数据。通过 .value 来读写:
import { ref } from 'vue'
const count = ref(0) // 基本类型
const user = ref({ name: 'Alice' }) // 对象
// 读取/修改时都要用 .value
console.log(count.value)
count.value++
user.value.name = 'Bob'
在模板中自动解包,不需要 .value:
<span>{{ count }}</span> <!-- 直接写 count,不用 count.value -->
2. reactive
直接包裹一个对象,将其整体变成响应式。不需要 .value,直接像原生对象一样读写:
import { reactive } from 'vue'
const state = reactive({ count: 0, name: 'Alice' })
state.count++ // 直接操作属性
state.name = 'Bob'
但 reactive 有两个容易踩坑的限制:
- 不能整体替换(
state = reactive({ count: 1 })会失去响应式)。 - 对象解构会丢失响应式(必须搭配
toRefs使用)。
3. toRef 和 toRefs
这两个 API 主要用于保持对象解构后的响应式连接:
const state = reactive({ x: 1, y: 2 })
// ❌ 直接解构会导致 x 和 y 失去响应式
const { x, y } = state
// ✅ 用 toRefs 保留响应式
const { x, y } = toRefs(state)
// 现在 x.value 修改会同步更新 state.x
toRef 用来单独为 props 或 reactive 对象的某个属性创建引用,常用于将某个属性传给组合式函数且保持其响应性。
4. unref
一个便捷工具:若参数是 ref 则取 .value,否则直接返回原值。写通用 composable 时非常实用。
计算属性:computed
computed 的用法与选项式 API 几乎一致,只是换成了函数调用形式:
const count = ref(2)
const double = computed(() => count.value * 2)
计算属性默认是只读的,也可以创建可写计算属性:
const double = computed({
get: () => count.value * 2,
set: (val) => { count.value = val / 2 }
})
double.value = 6 // count 自动变为 3
计算属性的缓存机制在组合式 API 中依然生效:只有当其依赖的响应式数据发生变化时,才会重新求值。
侦听器:watch、watchEffect、watchPostEffect
watch
用于手动指定一个或多个响应式数据源,并在它们变化时执行副作用:
const count = ref(0)
const name = ref('Alice')
// 侦听单个 ref
watch(count, (newVal, oldVal) => {
console.log(`count 从 ${oldVal} 变为 ${newVal}`)
})
// 侦听多个数据源
watch([count, name], ([newCount, newName], [oldCount, oldName]) => {
// ...
})
如果需要深度监听 reactive 对象内部属性的变化,需要显式传入 { deep: true }。当数据源是 ref 包裹的对象时,默认只监听引用变化,同样需要 deep。
watchEffect
它会自动收集在回调函数内部被访问过的响应式依赖,并在依赖变化时立即重新执行:
watchEffect(() => {
// 自动追踪 count.value 和 name.value
console.log(count.value, name.value)
})
与 watch 的区别:
watch是惰性的,初始不会执行;watchEffect在创建时立即执行一次。watchEffect无需手动指定依赖,依赖自动追踪(但无法获取旧值)。watchEffect更适合执行“只要数据变了就用最新数据做点事情”的场景,比如初始化请求、自动保存。
watchPostEffect
它是 watchEffect 的变种,保证在 DOM 更新完成后执行副作用,等同于 watchEffect 设置 { flush: 'post' }。常用于需要访问更新后的 DOM 的情况,防范时序问题。
生命周期钩子
组合式 API 的生命周期钩子形式是在 Vue 的 API 名称前加上 on 前缀。它们必须在 setup 期间调用(同步),并且不需要像选项式 API 那样把逻辑写在某个特定选项下。
import { onMounted, onUnmounted, onUpdated } from 'vue'
onMounted(() => {
console.log('组件已挂载')
})
onUnmounted(() => {
clearInterval(timer)
})
映射关系如下:
| 选项式 API | 组合式 API |
|----------------|------------------|
| beforeCreate | 直接在 setup 中写(不需对应钩子) |
| created | 直接在 setup 中写 |
| beforeMount | onBeforeMount |
| mounted | onMounted |
| beforeUpdate | onBeforeUpdate |
| updated | onUpdated |
| beforeUnmount | onBeforeUnmount |
| unmounted | onUnmounted |
| errorCaptured | onErrorCaptured |
注意,setup 本身就在 beforeCreate 和 created 之间执行,因此这两个阶段的初始化代码直接放在 <script setup> 顶层即可,无需对应的钩子。
真实应用场景举例
假设一个用户资料页面,需要加载用户数据并实时显示在线状态,同时支持搜索输入防抖。用组合式 API 可以把这些逻辑各自封装成清晰的组合式函数,然后在组件中“组装”它们:
<script setup>
import { useUser } from '@/composables/useUser'
import { useOnlineStatus } from '@/composables/useOnlineStatus'
import { useDebounce } from '@/composables/useDebounce'
const { user, loading } = useUser()
const { isOnline } = useOnlineStatus(user)
const { debouncedSearch, searchInput } = useDebounce()
</script>
每个功能点都是一个自包含的 JavaScript 模块,可单独测试,可在多个组件间复用——这正是组合式 API 带来的最大生产力提升。在简单的组件里你可以仍然用选项式 API,但当逻辑膨胀到需要“逻辑关注点分离”时,组合式 API 会让代码组织变得从容。