Vue3的响应式系统从Object.defineProperty全面迁移到ES6 Proxy,解决了Vue2中无法检测属性新增/删除和数组索引修改的局限性。Composition API配合reactive和ref构建响应式状态,通过track和effect机制实现细粒度依赖追踪。本文从Proxy代理机制、依赖收集与触发、ref与reactive实现原理三个层面解析Vue3响应式系统的运行机制,并提供自定义响应式实现的代码方案。
Proxy代理机制与响应式对象创建
Vue3使用Proxy代替Object.defineProperty实现响应式。Proxy拦截对象的全部13种操作(包括get、set、has、deleteProperty等),无需预先定义所有属性。reactive函数接收原始对象,返回一个Proxy代理对象。
// Vue3 reactive 核心:代理对象创建
const reactiveMap = new WeakMap()
function reactive(target) {
// 非对象类型直接返回
if (typeof target !== 'object' || target === null) {
return target
}
// 已代理过的对象直接返回缓存
if (reactiveMap.has(target)) {
return reactiveMap.get(target)
}
const proxy = new Proxy(target, {
get(target, key, receiver) {
if (key === '__isReactive') {
return true
}
track(target, key) // 依赖收集
const result = Reflect.get(target, key, receiver)
// 深层响应式:对象类型递归代理
if (typeof result === 'object' && result !== null) {
return reactive(result)
}
return result
},
set(target, key, value, receiver) {
const oldValue = target[key]
const result = Reflect.set(target, key, value, receiver)
// 值变化时触发更新
if (!Object.is(oldValue, value)) {
trigger(target, key)
}
return result
},
deleteProperty(target, key) {
const hadKey = key in target
const result = Reflect.deleteProperty(target, key)
if (hadKey && result) {
trigger(target, key)
}
return result
},
has(target, key) {
track(target, key)
return Reflect.has(target, key)
},
ownKeys(target) {
track(target, Symbol.iterator)
return Reflect.ownKeys(target)
}
})
reactiveMap.set(target, proxy)
return proxy
}
Reflect.get/set的使用保证代理行为与原始对象一致,receiver参数确保原型链上的getter/setter上下文正确。深层响应式通过get拦截器中对返回值递归调用reactive实现,这也是惰性的——只有被访问的属性才会被代理,避免初始化时的性能开销。
依赖收集与触发更新机制
Vue3的依赖收集基于effect函数和track/trigger两个核心操作。effect包装副作用函数,执行时自动收集其内部访问的响应式属性作为依赖;当属性变化时,trigger找到所有依赖该属性的effect并重新执行。
// 依赖映射表:target -> key -> Set
const targetMap = new WeakMap()
let activeEffect = null
// effect包装器
function effect(fn) {
const _effect = function() {
activeEffect = _effect
try {
fn()
} finally {
activeEffect = null
}
}
_effect.deps = [] // 记录该effect依赖的所有属性
_effect()
return _effect
}
// 依赖收集
function track(target, key) {
if (!activeEffect) return
let depsMap = targetMap.get(target)
if (!depsMap) {
depsMap = new Map()
targetMap.set(target, depsMap)
}
let dep = depsMap.get(key)
if (!dep) {
dep = new Set()
depsMap.set(key, dep)
}
if (!dep.has(activeEffect)) {
dep.add(activeEffect)
activeEffect.deps.push(dep) // 反向记录,便于清理
}
}
// 触发更新
function trigger(target, key) {
const depsMap = targetMap.get(target)
if (!depsMap) return
const dep = depsMap.get(key)
if (!dep) return
// 复制一份遍历,避免effect执行过程中修改Set导致无限循环
const effects = new Set(dep)
effects.forEach(effect => {
// 避免effect内部触发自身更新导致递归
if (effect !== activeEffect) {
effect()
}
})
}
targetMap使用WeakMap存储依赖关系,key为目标对象,value为Map(属性名到依赖集合的映射)。WeakMap的弱引用特性使得原始对象被回收时,对应的依赖关系自动清理,避免内存泄漏。
使用示例:
const state = reactive({ count: 0, name: 'Vue' })
// effect自动收集count依赖
effect(() => {
console.log('count changed:', state.count)
})
state.count = 1 // 输出: count changed: 1
state.name = 'Vue3' // 无输出(effect未依赖name)
ref实现原理与ref与reactive对比
ref用于包装基本类型值(也可以包装对象),通过.value访问器实现响应式。ref内部使用一个只有value属性的虚拟对象,对其调用reactive或自定义get/set拦截。
function ref(value) {
// 已是ref直接返回
if (isRef(value)) {
return value
}
// 对象类型直接使用reactive
const wrapper = {
get value() {
track(wrapper, 'value')
return value
},
set value(newValue) {
if (!Object.is(value, newValue)) {
value = newValue
trigger(wrapper, 'value')
}
}
}
wrapper.__isRef = true
return wrapper
}
function isRef(r) {
return r && r.__isRef === true
}
// 使用示例
const count = ref(0)
const user = ref({ name: 'Alice', age: 25 })
effect(() => {
console.log('count:', count.value)
})
count.value++ // 输出: count: 1
user.value.name = 'Bob' // ref包裹的对象内部修改也会触发响应
ref与reactive的核心区别:ref通过.value访问,reactive直接访问属性;ref可包装基本类型,reactive只能包装对象;ref包装对象时内部委托给reactive,实现深层响应。在Composition API中,ref更适合独立值状态管理,reactive更适合表单、配置等对象状态。
computed计算属性实现
computed基于effect实现,增加缓存和脏标记(dirty flag)机制。依赖未变化时返回缓存值,依赖变化时标记为脏,下次访问时重新计算。
function computed(getter) {
let value
let dirty = true
// 使用effect但设置lazy模式,不立即执行
const _effect = effect(getter, {
lazy: true,
scheduler: () => {
dirty = true // 依赖变化时标记为脏
// 通知依赖该computed的effect
trigger(wrapper, 'value')
}
})
const wrapper = {
get value() {
if (dirty) {
value = _effect() // 执行getter计算新值
dirty = false
}
track(wrapper, 'value') // 收集依赖此computed的effect
return value
},
__isRef: true
}
return wrapper
}
// 修改effect支持lazy选项
function effect(fn, options = {}) {
const _effect = function() {
activeEffect = _effect
try {
return fn()
} finally {
activeEffect = null
}
}
_effect.deps = []
_effect.scheduler = options.scheduler
if (!options.lazy) {
_effect() // 非lazy模式立即执行
}
return _effect
}
// 使用示例
const state = reactive({ firstName: '张', lastName: '三' })
const fullName = computed(() => `${state.firstName}${state.lastName}`)
effect(() => {
console.log('姓名:', fullName.value) // 姓名: 张三
})
state.lastName = '四' // 输出: 姓名: 张四
watch侦听器与副作用清理
watch监听响应式数据变化,执行回调函数。内部基于effect和旧值缓存实现,支持 oldValue参数传入回调。通过onCleanup注册副作用清理函数,避免竞态条件。
function watch(source, cb, options = {}) {
let oldValue
let cleanup
function onCleanup(fn) {
cleanup = fn
}
const _effect = effect(() => {
// 访问source收集依赖
const newValue = typeof source === 'function' ? source() : source.value
// 清理上一次的副作用
if (cleanup) cleanup()
if (!Object.is(oldValue, newValue)) {
cb(newValue, oldValue, onCleanup)
oldValue = newValue
}
}, {
lazy: true,
scheduler: () => {
// 依赖变化时触发effect重新执行
_effect()
}
})
// 初始化获取旧值
oldValue = _effect()
// immediate选项
if (options.immediate) {
cb(oldValue, undefined, onCleanup)
}
}
// 使用示例
const keyword = ref('')
watch(keyword, async (newVal, oldVal, onCleanup) => {
const controller = new AbortController()
onCleanup(() => controller.abort())
const res = await fetch(`/api/search?q=${newVal}`, {
signal: controller.signal
})
console.log(await res.json())
})
keyword.value = 'Vue3' // 发起请求
keyword.value = 'Vue4' // 中断上一个请求,发起新请求
watch的onCleanup机制解决了异步操作中的竞态问题:当快速连续修改keyword时,前一个请求被abort,只有最后一个请求的结果会被处理。这在搜索联想、自动保存等场景中非常实用。
响应式系统性能优化
Vue3响应式系统在设计层面做了多项性能优化:
1. 惰性响应式:reactive对象的属性在被访问时才递归代理,避免初始化时遍历整个对象树。
2. 依赖清理:effect执行前清理上一轮的依赖关系(通过deps数组),处理条件分支导致的依赖变化。例如effect内部有if/else分支,条件变化后不再访问的属性需要从依赖中移除。
// 依赖清理实现
function cleanupEffect(effect) {
effect.deps.forEach(dep => {
dep.delete(effect)
})
effect.deps.length = 0
}
function effect(fn, options = {}) {
const _effect = function() {
activeEffect = _effect
cleanupEffect(_effect) // 清理旧依赖
try {
return fn()
} finally {
activeEffect = null
}
}
// ...
}
// track中更新反向追溯
function track(target, key) {
if (!activeEffect) return
// ...
if (!dep.has(activeEffect)) {
dep.add(activeEffect)
activeEffect.deps.push(dep)
}
}
3. 批量更新:多次状态修改在同一事件循环中只触发一次DOM更新。Vue3通过jobQueue和flushJobs实现微任务级别的批量调度。
const queue = new Set()
let isFlushing = false
function queueJob(job) {
queue.add(job) // Set自动去重
if (!isFlushing) {
isFlushing = true
Promise.resolve().then(flushJobs)
}
}
function flushJobs() {
try {
queue.forEach(job => job())
} finally {
queue.clear()
isFlushing = false
}
}
// trigger中使用队列
function trigger(target, key) {
// ...
effects.forEach(effect => {
if (effect.options.scheduler) {
queueJob(effect.options.scheduler)
} else {
effect()
}
})
}
批量更新确保了连续修改多个响应式属性时不会重复执行渲染effect,将多次DOM操作合并为一次。这是Vue3渲染性能优于Vue2的关键因素之一。
原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/vue3compositionapi-xiang-ying-shi-xi-tong-proxy-dai-li-ji/