Vue3响应式原理深度解析:Proxy与Ref的实现机制

Vue3的响应式系统是整个框架的核心。相比Vue2基于Object.defineProperty的实现,Vue3改用ES6 Proxy重写了响应式引擎,解决了数组监听、属性新增删除、深层嵌套等历史问题。理解响应式原理对排查响应式失效、优化渲染性能至关重要。

从Object.defineProperty到Proxy的演进

Vue2的响应式通过Object.defineProperty递归遍历data对象的每个属性,为每个属性设置getter和setter。这个方案有三个硬伤:无法检测属性的新增和删除(需要Vue.set/Vue.delete);无法直接监听数组索引修改和length变化(通过重写数组原型方法绕过);初始化时递归遍历所有属性,深层对象初始化开销大。

Vue3用Proxy替代了Object.defineProperty。Proxy拦截整个对象而非单个属性,天然支持属性增删检测和数组操作监听。且Proxy是惰性的——只有访问到某个属性时才递归代理其子对象,初始化时只创建一层代理。

reactive函数源码级解析

reactive是Vue3最基础的响应式API。它的核心是createReactiveObject函数,通过Proxy的handler实现依赖收集和触发更新。

// 简化的reactive实现
const reactiveMap = new WeakMap()

function reactive(target) {
    if (target.__v_isReactive) return target
    
    const existingProxy = reactiveMap.get(target)
    if (existingProxy) return existingProxy
    
    const proxy = new Proxy(target, {
        get(target, key, receiver) {
            if (key === '__v_isReactive') return true
            
            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 (!Object.is(oldValue, value)) {
                trigger(target, key)
            }
            return result
        },
        deleteProperty(target, key) {
            const result = Reflect.deleteProperty(target, key)
            trigger(target, key)
            return result
        }
    })
    
    reactiveMap.set(target, proxy)
    return proxy
}

track函数在getter中执行,将当前正在执行的副作用函数(effect)与被读取的属性建立映射关系。这个映射存储在全局的targetMap中,结构为WeakMap -> Map -> Set:

// 依赖收集的数据结构
let activeEffect = null
const targetMap = new WeakMap()

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)
}

function trigger(target, key) {
    const depsMap = targetMap.get(target)
    if (!depsMap) return
    
    const dep = depsMap.get(key)
    if (dep) {
        // 拷贝后遍历,防止遍历过程中Set被修改
        [...dep].forEach(effect => effect())
    }
}

ref与reactive的区别与选择

reactive只能代理对象类型(Object、Array、Map、Set),对于基本类型(string、number、boolean)无能为力。ref通过对象包装的方式解决这个问题——将基本类型值存放在.value属性上,对.value做依赖收集。

function ref(value) {
    return {
        get value() {
            track(this, 'value')
            return value
        },
        set value(newVal) {
            if (!Object.is(value, newVal)) {
                value = newVal
                trigger(this, 'value')
            }
        }
    }
}

实际使用中,ref和reactive的选择遵循以下原则:基本类型用ref,对象类型看场景。如果整个对象会被整体替换(如API返回后重新赋值),用ref更方便,因为reactive返回的代理对象不能直接替换。如果对象内容逐步修改,reactive语法更简洁。

// ref方案:对象整体替换
const user = ref({ name: '张三', age: 25 })
user.value = await fetchUser()  // 整体替换,响应式正常

// reactive方案:逐步修改
const state = reactive({ user: null, loading: false })
state.loading = true
state.user = await fetchUser()  // 修改属性,响应式正常
state = { user: null, loading: false }  // 错误!直接替换失去响应式

computed与watch的实现原理

computed本质上是一个带缓存的响应式依赖。它内部维护一个dirty标志,只有当依赖的响应式数据变化时才将dirty设为true,下次访问时才重新计算。

function computed(getter) {
    let value
    let dirty = true
    
    const effect = createReactiveEffect(() => {
        value = getter()
        dirty = false
    })
    
    effect.run()  // 首次执行,建立依赖关系
    
    return {
        get value() {
            if (dirty) {
                effect.run()
            }
            track(this, 'value')
            return value
        }
    }
}

watch的实现基于effect和onCleanup回调。watch监听一个getter函数或ref,在其值变化时执行回调函数。关键在于新旧值的比较逻辑和副作用清理机制。

// watch的基本用法
const count = ref(0)

watch(count, (newVal, oldVal, onCleanup) => {
    const timer = setTimeout(() => {
        console.log('debounced:', newVal)
    }, 300)
    
    // 清理上一次的副作用
    onCleanup(() => clearTimeout(timer))
})

// watchEffect自动收集依赖
watchEffect((onCleanup) => {
    // 自动追踪内部用到的所有响应式数据
    console.log('count changed:', count.value)
})

响应式性能优化技巧

大型应用中,过度的响应式追踪会导致性能问题。以下优化手段值得注意:

shallowRef/shallowReactive:只对第一层做响应式,深层访问不递归代理,适合大对象列表的场景。

const largeList = shallowRef([])
// 只追踪.value的赋值,内部数组修改需手动触发
largeList.value = newArray  // 响应式触发
largeList.value.push(item)   // 不会触发更新,需要替换整个数组

// 使用triggerRef手动触发
largeList.value.push(item)
triggerRef(largeList)  // 手动通知更新

markRaw:标记对象永远不被转为响应式,适合第三方实例、Canvas对象等不需要响应式的数据。

const chartInstance = markRaw(echarts.init(dom))
// echarts实例不会被Proxy包装,避免性能浪费和潜在bug

v-once和v-memo:v-once让元素只渲染一次,后续更新跳过。v-memo根据条件缓存子树,只有依赖数组变化时才重新渲染。

<!-- 只渲染一次,后续更新不参与diff -->
<div v-once>{{ heavyCompute() }}</div>

<!-- 只有item.id或item.selected变化时才重新渲染 -->
<div v-for="item in list" :key="item.id" v-memo="[item.id, item.selected]">
    <span>{{ item.name }}</span>
    <span>{{ item.detail }}</span>
</div>

调试响应式问题时,Vue Devtools的Components面板可以查看组件的响应式数据结构和依赖关系。Vue3还提供了toRaw函数获取代理对象的原始对象,用于判断响应式状态。

原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/vue3-xiang-ying-shi-yuan-li-shen-du-jie-xi-proxy-yu-ref-de/

(0)
小编小编
上一篇 14小时前
下一篇 14小时前

相关推荐