Vue3的响应式系统基于ES6 Proxy实现,相比Vue2的Object.defineProperty方案,能拦截更多操作类型且性能更优。理解Proxy与effect的配合机制,有助于在复杂场景下编写高效的前端代码,避免不必要的重复渲染。
Proxy拦截器与传统defineProperty的差异
Vue2使用Object.defineProperty劫持对象属性,存在三个固有限制:无法监听属性新增/删除(需用Vue.set/Vue.delete)、无法监听数组索引修改、需要递归遍历对象所有属性初始化劫持。Vue3改用Proxy构造函数,在对象层面整体代理,天然支持属性增删和数组操作,且采用惰性代理——只有被访问的属性才会递归转为响应式。
reactive函数的代理实现
Vue3的reactive函数返回目标对象的Proxy代理。核心拦截逻辑如下:
const targetMap = new WeakMap() // 存储依赖关系
let activeEffect = null // 当前正在执行的effect
function reactive(target) {
const handler = {
get(target, key, receiver) {
const result = Reflect.get(target, key, receiver)
track(target, key) // 依赖收集
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 (oldValue !== value) {
trigger(target, key) // 触发更新
}
return result
},
deleteProperty(target, key) {
const result = Reflect.deleteProperty(target, key)
trigger(target, key)
return result
}
}
return new Proxy(target, handler)
}
WeakMap的key是原始对象,value是Map。Map的key是属性名,value是Set集合,存储该属性关联的所有effect函数。WeakMap的选择是为了在原始对象被GC回收时自动清理依赖。
effect函数与依赖收集
effect函数包裹一个回调,执行时把自身设为activeEffect,执行回调过程中触发属性get操作时完成依赖收集:
function effect(fn) {
const _effect = function() {
activeEffect = _effect
fn()
activeEffect = null
}
_effect()
return _effect
}
function track(target, key) {
if (activeEffect === null) 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)
}
dep.add(activeEffect)
}
function trigger(target, key) {
const depsMap = targetMap.get(target)
if (!depsMap) return
const dep = depsMap.get(key)
if (dep) {
dep.forEach(effect => effect())
}
}
当属性被修改时,trigger函数取出该属性关联的effect集合并逐一执行,实现数据变更到视图更新的自动驱动。Vue3内部用effect实现了computed、watch和组件render。
ref与reactive的选择策略
ref和reactive都能创建响应式数据,但适用场景不同:
// ref:适合基本类型,通过.value访问
const count = ref(0)
count.value++
// reactive:适合对象/数组,直接访问属性
const state = reactive({
user: { name: '张三', age: 25 },
list: [1, 2, 3]
})
state.user.name = '李四' // 响应式生效
state.list.push(4) // 响应式生效
// ref持有对象时内部自动用reactive代理
const userRef = ref({ name: '张三' })
userRef.value.name = '李四' // 响应式生效
// 解构reactive会丢失响应式
const { user } = state
user.name = '王五' // 响应式丢失
// 使用toRefs保持解构后的响应式
const { user } = toRefs(state)
user.value.name = '王五' // 响应式保持
工程实践中,推荐单一对象用reactive,零散的基本类型用ref。组件内状态管理推荐把相关状态归入同一个reactive对象,减少解构带来的响应式丢失风险。
computed缓存机制分析
computed基于effect实现,但加入了缓存和脏标记机制。只有依赖值变化时才重新计算:
function computed(getter) {
let value, dirty = true
const _effect = effect(() => {
if (dirty) {
value = getter()
dirty = false
}
}, {
scheduler: () => {
dirty = true
trigger(computedObj, 'value')
}
})
const computedObj = {
get value() {
track(computedObj, 'value')
if (dirty) {
value = getter()
dirty = false
}
return value
}
}
return computedObj
}
scheduler选项让effect在被trigger时执行自定义逻辑而非重新运行effect。computed利用这一点实现lazy evaluation——只有访问.value且dirty为true时才重新计算。高开销计算场景下,computed比methods性能更优。
响应式性能优化建议
大型应用中响应式性能需关注以下几点。避免在template中创建过多响应式对象;大列表渲染优先用shallowRef避免深层代理开销;只读数据用readonly包裹后不会被代理。组件库设计时,合理使用markRaw跳过代理,对第三方库实例(地图、图表对象等)能显著减少内存占用。
原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/vue3-xiang-ying-shi-yuan-li-shen-ru-pou-xi-proxy-yu-effect/