Vue3的组合式API(Composition API)重构了组件逻辑的组织方式,解决了Options API在复杂组件中逻辑分散的问题。理解响应式系统的工作原理,是编写高性能Vue3应用的基础。本文从响应式机制到composable函数设计,覆盖工程实践中最核心的API用法和设计模式。
Vue3响应式系统核心原理:Proxy与依赖追踪
Vue3使用ES6 Proxy替代了Vue2的Object.defineProperty实现响应式。Proxy能拦截对象的所有属性操作,包括新增和删除属性,无需像Vue2那样使用$set。
响应式系统的核心是三个概念的协作:reactive(代理对象)、effect(副作用函数)和track/trigger(依赖收集与触发)。
// reactive的简化实现原理
const targetMap = new WeakMap()
function reactive(target) {
const handler = {
get(obj, key, receiver) {
const result = Reflect.get(obj, key, receiver)
track(target, key) // 依赖收集
if (typeof result === 'object' && result !== null) {
return reactive(result) // 深层代理
}
return result
},
set(obj, key, value, receiver) {
const oldValue = obj[key]
const result = Reflect.set(obj, key, value, receiver)
if (oldValue !== value) {
trigger(target, key) // 触发更新
}
return result
}
}
return new Proxy(target, handler)
}
function track(target, key) {
if (!activeEffect) return
let depsMap = targetMap.get(target)
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()))
}
let dep = depsMap.get(key)
if (!dep) {
depsMap.set(key, (dep = new Set()))
}
dep.add(activeEffect)
}
ref和reactive的区别在于:reactive代理整个对象,适合对象/数组;ref通过.value访问,适合基本类型和需要整体替换的场景。ref内部对对象类型也会调用reactive进行深层代理。
import { ref, reactive, isRef, isReactive } from 'vue'
// 基本类型用ref
const count = ref(0)
console.log(count.value) // 0
count.value++
console.log(count.value) // 1
// 对象/数组用reactive
const state = reactive({
user: { name: '张三', age: 28 },
list: [1, 2, 3]
})
state.user.name = '李四' // 自动触发更新
// 判断类型
console.log(isRef(count)) // true
console.log(isReactive(state)) // true
computed与watch的精确控制
computed是带缓存的响应式计算属性。只有当依赖的响应式数据变化时才会重新计算,否则返回缓存值。
import { ref, computed, watch, watchEffect } from 'vue'
const price = ref(99.9)
const quantity = ref(3)
const discount = ref(0.8)
// computed默认只读,可写版本用get/set
const total = computed({
get: () => price.value * quantity.value * discount.value,
set: (newVal) => {
// 反向推导折扣
discount.value = newVal / (price.value * quantity.value)
}
})
// watch监听特定响应式源
watch(
() => state.user.age,
(newAge, oldAge) => {
console.log(`年龄从 ${oldAge} 变为 ${newAge}`)
},
{
immediate: true, // 立即执行一次
deep: true, // 深度监听(对象内部变化)
flush: 'post', // 在DOM更新后执行
}
)
// watchEffect自动收集依赖,无需显式声明
watchEffect(() => {
console.log(`当前总价: ${total.value}`)
})
watch与watchEffect的选择原则:需要访问旧值或需要明确指定监听源时用watch;只需副作用自动追踪依赖时用watchEffect。watchEffect的清理函数通过onCleanup注册:
watchEffect((onCleanup) => {
const timer = setInterval(() => {
console.log('polling...', count.value)
}, 1000)
onCleanup(() => {
clearInterval(timer)
})
})
Composable函数设计模式与最佳实践
Composable函数是组合式API的核心复用单元。命名以use开头,接受响应式输入并返回响应式输出。一个设计良好的composable应该做到:输入输出都是响应式的、不产生副作用、可独立测试。
// useMouse.ts - 鼠标位置追踪
import { ref, onMounted, onUnmounted, type Ref } from 'vue'
interface MousePosition {
x: Ref<number>
y: Ref<number>
}
export function useMouse(): MousePosition {
const x = ref(0)
const y = ref(0)
const update = (e: MouseEvent) => {
x.value = e.pageX
y.value = e.pageY
}
onMounted(() => {
window.addEventListener('mousemove', update)
})
onUnmounted(() => {
window.removeEventListener('mousemove', update)
})
return { x, y }
}
// useFetch.ts - 数据请求封装
import { ref, watch, isRef, unref, type Ref } from 'vue'
export function useFetch<T>(
url: string | Ref<string>,
options?: RequestInit
) {
const data = ref<T | null>(null)
const error = ref<string | null>(null)
const loading = ref(false)
async function doFetch() {
loading.value = true
error.value = null
data.value = null
try {
const res = await fetch(unref(url), options)
if (!res.ok) throw new Error(`HTTP ${res.status}`)
data.value = await res.json() as T
} catch (e) {
error.value = (e as Error).message
} finally {
loading.value = false
}
}
// 如果url是响应式引用,自动重新请求
if (isRef(url)) {
watch(url, doFetch, { immediate: true })
} else {
doFetch()
}
return { data, error, loading, refresh: doFetch }
}
在组件中使用composable:
<script setup lang="ts">
import { useMouse, useFetch } from './composables'
interface User {
id: number
name: string
email: string
}
const { x, y } = useMouse()
const { data: users, loading, error, refresh } = useFetch<User[]>(
'https://api.example.com/users'
)
</script>
<template>
<div>
<p>鼠标位置: ({{ x }}, {{ y }})</p>
<button @click="refresh" :disabled="loading">
{{ loading ? '加载中' : '刷新' }}
</button>
<ul v-if="users">
<li v-for="user in users" :key="user.id">
{{ user.name }} - {{ user.email }}
</li>
</ul>
<p v-if="error" class="error">{{ error }}</p>
</div>
</template>
组件通信模式与provide/inject类型安全
组合式API下,组件通信推荐使用provide/inject配合InjectionKey实现类型安全。相比props/emit的逐层传递,provide/inject适合跨层级共享状态。
// keys.ts - 定义类型安全的InjectionKey
import type { InjectionKey, Ref } from 'vue'
import { provide, inject, ref } from 'vue'
export interface ThemeContext {
theme: Ref<'light' | 'dark'>
toggle: () => void
}
export const ThemeKey: InjectionKey<ThemeContext> = Symbol('theme')
// Provider组件
export function provideTheme() {
const theme = ref<'light' | 'dark'>('light')
const toggle = () => {
theme.value = theme.value === 'light' ? 'dark' : 'light'
}
provide(ThemeKey, { theme, toggle })
return { theme, toggle }
}
// Consumer组件
export function useTheme(): ThemeContext {
const ctx = inject(ThemeKey)
if (!ctx) {
throw new Error('useTheme must be used within a ThemeProvider')
}
return ctx
}
性能优化:shallowRef与markRaw的使用场景
Vue3默认对reactive对象进行深层代理,大对象或无需响应式的数据会带来不必要的性能开销。shallowRef和markRaw可以跳过响应式代理。
import { shallowRef, markRaw, triggerRef } from 'vue'
// shallowRef:只有.value替换时才触发更新,不代理内部属性
const largeList = shallowRef<HeavyItem[]>([])
// 修改内部属性不会触发更新
largeList.value.push(newItem) // 不触发
// 需手动触发
triggerRef(largeList)
// 或整体替换
largeList.value = [...largeList.value, newItem] // 触发
// markRaw:标记对象永不转为响应式
const mapInstance = markRaw(new HeavyMap())
const chartConfig = reactive({
chart: mapInstance // 不会被代理
})
典型场景:ECharts实例、Map/Set集合、第三方库对象实例。这些对象不需要Vue的响应式追踪,标记为markRaw可避免Proxy包装导致的性能问题和潜在的循环引用。
组合式API的设计哲学是将组件逻辑按功能聚合而非按选项分散。通过composable函数提取复用逻辑,配合TypeScript类型推导和InjectionKey类型安全,可以构建出结构清晰、可维护性强的Vue3应用。实际项目中建议将composable按领域分目录管理,每个文件只导出一个函数,保持单一职责。
原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/vue3-zu-he-shi-api-shi-zhan-xiang-ying-shi-xi-tong-yuan-li/