Vue3的组合式API(Composition API)不只是Options API的语法替代,它是一套全新的逻辑组织和复用范式。ref、reactive、computed、watch这些API背后是Proxy驱动的响应式依赖追踪系统。理解这个系统的运作机制,才能写出性能合理的组合式逻辑,避免响应式陷阱。本文从响应式原理入手,再到组合式函数(Composable)的设计模式和常见踩坑。
Proxy驱动的响应式依赖追踪机制
Vue3用Proxy替代了Vue2的Object.defineProperty,解决了三个核心问题:动态添加属性的监听、数组索引和length的监听、Map/Set的响应式支持。响应式系统的核心流程分三步:属性读取时收集依赖、属性修改时触发更新、批量更新通过微任务队列合并。
简化版的reactive实现逻辑:
// 极简版 reactive 实现原理(仅用于理解,非Vue3源码)
const targetMap = new WeakMap() // target -> depsMap
let activeEffect = null
function reactive(target) {
return new Proxy(target, {
get(obj, key, receiver) {
// 依赖收集:当前活跃的effect记为key的依赖
if (activeEffect) {
let depsMap = targetMap.get(obj)
if (!depsMap) {
depsMap = new Map()
targetMap.set(obj, depsMap)
}
let dep = depsMap.get(key)
if (!dep) {
dep = new Set()
depsMap.set(key, dep)
}
dep.add(activeEffect)
}
return Reflect.get(obj, key, receiver)
},
set(obj, key, value, receiver) {
const result = Reflect.set(obj, key, value, receiver)
// 触发更新:找到key的所有依赖并执行
const depsMap = targetMap.get(obj)
if (depsMap) {
const dep = depsMap.get(key)
if (dep) {
dep.forEach(effect => effect())
}
}
return result
}
})
}
function effect(fn) {
activeEffect = fn
fn() // 执行时触发get,完成依赖收集
activeEffect = null
}
ref与reactive的区别在于ref用闭包+class的get/set实现原始值的响应式,而reactive只能处理对象。ref内部包装了一层带value属性的RefImpl类,读取.value时收集依赖,设置.value时触发更新。模板中自动解包.value是编译器做的语法糖。
Composable设计模式与逻辑复用
Composable是组合式API的核心复用单元。一个好的Composable遵循”单一职责、显式参数、清理副作用”三原则。常见的设计模式:
状态封装模式——将状态和操作打包对外暴露:
// useCounter.ts
import { ref, computed } from 'vue'
export function useCounter(initialValue = 0) {
const count = ref(initialValue)
const doubled = computed(() => count.value * 2)
function increment(delta = 1) {
count.value += delta
}
function decrement(delta = 1) {
count.value -= delta
}
function reset() {
count.value = initialValue
}
return { count, doubled, increment, decrement, reset }
}
异步数据模式——封装请求状态管理:
// useFetch.ts
import { ref, toValue, watchEffect } from 'vue'
export function useFetch(url, options = {}) {
const data = ref(null)
const error = ref(null)
const isPending = ref(false)
let controller = null
async function execute() {
// 取消上一次未完成的请求
if (controller) controller.abort()
controller = new AbortController()
data.value = null
error.value = null
isPending.value = true
try {
const resolvedUrl = toValue(url) // 支持ref/string/getter
const response = await fetch(resolvedUrl, {
...options,
signal: controller.signal,
})
if (!response.ok) {
throw new Error(`HTTP ${response.status}`)
}
data.value = await response.json()
} catch (e) {
if (e.name !== 'AbortError') {
error.value = e
}
} finally {
isPending.value = false
}
}
// 自动响应url变化
watchEffect(() => {
execute()
})
// 暴露手动刷新能力
return { data, error, isPending, refresh: execute }
}
事件监听模式——自动注册和清理副作用:
// useWindowSize.ts
import { ref, onMounted, onUnmounted } from 'vue'
export function useWindowSize() {
const width = ref(window.innerWidth)
const height = ref(window.innerHeight)
function handler() {
width.value = window.innerWidth
height.value = window.innerHeight
}
onMounted(() => window.addEventListener('resize', handler))
onUnmounted(() => window.removeEventListener('resize', handler))
return { width, height }
}
响应式系统常见陷阱与排查方法
陷阱一:解构丢失响应式。reactive对象被解构后,变量变成普通值,不再追踪变化:
// 错误:解构后丢失响应式
const state = reactive({ count: 0, name: 'test' })
let { count, name } = state // count和name是普通值
// 正确做法1:用toRefs保持引用
const { count, name } = toRefs(state) // 每个属性变成ref
// 正确做法2:直接使用reactive对象
state.count++ // 响应式生效
陷阱二:在reactive中替换整个对象。直接赋新对象会断开Proxy代理链路:
const state = reactive({ list: [], info: {} })
// 错误:替换整个对象断开响应式
state.info = { name: 'new' } // 这个是可以的,修改属性
// state = reactive({ list: [], info: {} }) // 这不行,重新赋值变量
// 如果需要替换整个对象的属性,用Object.assign
Object.assign(state, { list: [], info: { name: 'new' } })
陷阱三:watch监听reactive对象属性时未用getter函数。直接传reactive对象的属性值给watch,只捕获了初始值:
const state = reactive({ count: 0 })
// 错误:只获取了初始值0
watch(state.count, (val) => console.log(val))
// 正确:用getter函数
watch(
() => state.count,
(val, oldVal) => console.log(val, oldVal)
)
// 监听整个reactive对象时,deep默认开启
watch(state, (newState) => {
// newState是同一个Proxy引用,需要手动对比或用getter
})
性能优化:shallowRef与markRaw的选择
深层响应式(reactive/ref)会对对象做递归Proxy代理,大对象的开销不可忽略。当只关心对象整体替换而非内部属性变化时,使用shallowRef:
import { shallowRef, triggerRef } from 'vue'
// 大型列表数据,只关心整体替换
const hugeList = shallowRef([])
// 异步加载数据
async function loadPage(page) {
const data = await fetchList(page)
hugeList.value = data // 整体替换,触发更新
}
// 如果修改了内部属性又想触发更新,手动调用triggerRef
hugeList.value[0].name = 'changed'
triggerRef(hugeList) // 强制触发依赖更新
// 第三方库实例永远不需要响应式追踪
import * as echarts from 'echarts'
const chartInstance = markRaw(echarts.init(dom))
// 或者在reactive中使用shallowRef
const state = reactive({
chart: shallowRef(null)
})
组合式API的精髓不在于ref和reactive的写法差异,而在于逻辑可组合、可复用、可测试的架构能力。理解响应式原理是正确使用的前提,设计好Composable是高效开发的基础。
原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/vue3-zu-he-shi-api-she-ji-mo-shi-yu-xiang-ying-shi-xi-tong/