Vue 3 的发布带来了许多令人兴奋的新特性,其中最引人注目的莫过于组合式 API(Composition API)。它从根本上改变了我们组织组件逻辑的方式,让代码复用和类型推导变得更加自然。本文将带你从 setup 函数出发,逐步深入响应式系统的底层原理,最后通过实战案例展示如何优雅地使用组合式 API。
为什么需要组合式 API?
在 Vue 2 中,我们使用选项式 API(Options API)来组织代码:data 放数据、methods 放方法、computed 放计算属性。当组件变得复杂时,一个功能的逻辑会被分散到多个选项中,导致代码难以阅读和维护。
组合式 API 允许我们按逻辑功能组织代码,而不是按选项类型。所有与某个功能相关的响应式数据、计算属性、方法和生命周期钩子都可以写在一起,甚至可以抽离到独立的组合函数中。
setup 函数:组合式 API 的入口
setup 是组合式 API 的起点,它在组件实例创建之前执行,因此无法访问 this。它接收两个参数:
props:组件的响应式 props 对象context:包含attrs、slots、emit和expose的上下文对象
import { ref, computed } from 'vue'
export default {
props: {
initialCount: { type: Number, default: 0 }
},
setup(props, { emit }) {
const count = ref(props.initialCount)
const double = computed(() => count.value * 2)
function increment() {
count.value++
emit('change', count.value)
}
return { count, double, increment }
}
}
需要注意的是,setup 返回的对象中的属性会自动暴露给模板使用。如果使用 <script setup> 语法糖,则可以省略显式的 return,代码更加简洁。
响应式核心:ref 与 reactive
Vue 3 提供了两个创建响应式数据的 API:
ref:包装任意值
ref 接收一个内部值并返回一个响应式的、可变的 ref 对象,该对象只有一个 .value 属性指向内部值。
import { ref } from 'vue'
const count = ref(0)
console.log(count.value) // 0
count.value++
console.log(count.value) // 1
在模板中,Vue 会自动解包 ref,因此可以直接写 {{ count }} 而不需要 .value。
reactive:代理对象
reactive 返回一个对象的响应式代理,它基于 ES6 的 Proxy 实现,可以深度监听对象属性的变化。
import { reactive } from 'vue'
const state = reactive({
name: 'Vue 3',
version: 3
})
state.version = 3.4 // 触发更新
选择建议:基本类型用 ref,对象类型两者皆可,但 reactive 在解构时会丢失响应性,此时应配合 toRefs 使用。
响应式原理:Proxy 与依赖收集
Vue 3 响应式系统的核心可以用一句话概括:在读取时收集依赖,在修改时触发更新。
Proxy 代理
与 Vue 2 使用 Object.defineProperty 不同,Vue 3 使用 Proxy 来拦截对象的操作。Proxy 可以拦截多达 13 种操作,包括属性读取、赋值、删除、in 操作符等,并且能完美支持数组索引和 Map、Set 等集合类型。
const target = { count: 0 }
const proxy = new Proxy(target, {
get(target, key, receiver) {
track(target, key) // 收集依赖
return Reflect.get(target, key, receiver)
},
set(target, key, value, receiver) {
const result = Reflect.set(target, key, value, receiver)
trigger(target, key) // 触发更新
return result
}
})
依赖收集与触发
Vue 3 内部维护了一个 WeakMap 结构来存储依赖关系:
- targetMap:
WeakMap<target, Map<key, Dep>> - Dep:一个
Set,存储所有依赖该属性的副作用函数
当执行 effect(如组件的渲染函数、watchEffect 回调)时,会先将当前 effect 设置为全局的 activeEffect,然后在读取响应式属性时,track 函数会将 activeEffect 添加到对应属性的 Dep 中。当属性被修改时,trigger 函数会从 Dep 中取出所有 effect 并依次执行。
这种设计使得依赖收集是精确到属性级别的,避免了 Vue 2 中全量重新渲染的性能问题。
生命周期钩子与 watch
在 setup 中,生命周期钩子需要以 on 开头导入使用:
import { onMounted, onUnmounted, watch, watchEffect } from 'vue'
onMounted(() => {
console.log('组件已挂载')
})
onUnmounted(() => {
console.log('组件已卸载')
})
// watch:显式指定源
watch(count, (newVal, oldVal) => {
console.log(`count 从 ${oldVal} 变为 ${newVal}`)
})
// watchEffect:自动收集依赖
watchEffect(() => {
console.log(`当前 count 为 ${count.value}`)
})
watchEffect 会立即执行一次,并自动追踪函数体内用到的响应式依赖;而 watch 需要显式指定监听源,并且可以获取新旧值。
实战:封装一个 useFetch 组合函数
组合式 API 最大的优势在于逻辑复用。下面我们封装一个通用的数据请求组合函数:
import { ref, watchEffect, toValue } from 'vue'
export function useFetch(url) {
const data = ref(null)
const error = ref(null)
const loading = ref(false)
watchEffect(async () => {
loading.value = true
data.value = null
error.value = null
try {
const res = await fetch(toValue(url))
if (!res.ok) throw new Error(res.statusText)
data.value = await res.json()
} catch (e) {
error.value = e
} finally {
loading.value = false
}
})
return { data, error, loading }
}
在组件中使用:
<script setup>
import { ref } from 'vue'
import { useFetch } from './useFetch'
const id = ref(1)
const { data, error, loading } = useFetch(() => `/api/posts/${id.value}`)
</script>
<template>
<div v-if="loading">加载中...</div>
<div v-else-if="error">出错了:{{ error.message }}</div>
<article v-else>{{ data.title }}</article>
</template>
这个例子展示了组合式 API 的威力:响应式数据、副作用逻辑和状态管理被封装在一个可复用的函数中,任何组件都可以直接使用。
总结
组合式 API 不仅是一种新的代码组织方式,更是 Vue 3 响应式系统能力的完全释放。从 setup 入口到 ref/reactive 的数据定义,从 Proxy 依赖收集到 effect 触发更新,每一层设计都体现了对开发体验和运行性能的深度考量。掌握组合式 API,意味着你能够写出更清晰、更可复用、类型更友好的 Vue 应用。
未经允许不得转载:任鹏个人博客 » Vue 3 组合式 API 深度解析:从 setup 到响应式原理的完整指南


朋友圈点赞图在线生成源码