Vue3响应式原理深度解析:Proxy与reactive实现机制及常见陷阱

Vue3响应式系统的核心:Proxy代理

Vue3用Proxy替代了Vue2的Object.defineProperty实现响应式。Proxy能拦截对象的所有属性操作,包括新增属性和删除属性,而Object.defineProperty只能拦截已声明属性的get/set。这是Vue3响应式系统的底层基础。

Proxy的handler提供13种拦截器(trap),Vue3主要使用get、set、has、deleteProperty、ownKeys五种。reactive函数的核心实现:

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

function reactive(target) {
    if (typeof target !== 'object' || target === null) return 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 res = Reflect.get(target, key, receiver)
            track(target, key)
            if (typeof res === 'object' && res !== null) {
                return reactive(res)
            }
            return res
        },
        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
        }
    })
    
    reactiveMap.set(target, proxy)
    return proxy
}

reactiveMap用WeakMap缓存原始对象到代理对象的映射,避免对同一对象重复创建Proxy。WeakMap的弱引用特性使得原始对象被回收时,缓存自动清理,不会造成内存泄漏。

依赖收集与触发更新:track与trigger

响应式系统的核心是建立”属性读取到副作用函数”的映射关系。track在属性被读取时收集当前正在执行的副作用函数,trigger在属性被修改时执行所有收集到的副作用函数。

let activeEffect = null

const targetMap = new WeakMap()

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

function effect(fn) {
    activeEffect = fn
    fn()
    activeEffect = null
}

targetMap是三层结构:WeakMap到Map到Set。第一层以原始对象为key,第二层以属性名为key,第三层Set存储所有依赖该属性的副作用函数。当activeEffect为null时(非effect上下文中读取属性),不进行依赖收集。

ref与reactive的使用场景区分

ref和reactive是Vue3两种响应式API,适用场景不同。ref用于基本类型值(String、Number、Boolean),reactive用于对象和数组。

ref内部实际是一个包含value属性的包装对象,通过Object.defineProperty的get/set实现响应式(对.value的访问触发track/trigger)。ref包装对象时,.value返回的是reactive代理对象。

import { ref, reactive, watchEffect } from 'vue'

// ref适合基本类型
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)  // 触发更新

// watchEffect自动追踪响应式依赖
watchEffect(() => {
    console.log(`count: ${count.value}, name: ${state.user.name}`)
})

模板中ref不需要.value(自动解包),但在JS代码中必须使用.value。reactive对象解构会丢失响应式,需用toRefs转换:

import { reactive, toRefs } from 'vue'

const state = reactive({ count: 0, name: 'test' })

// 错误:解构后失去响应式
const { count, name } = state

// 正确:使用toRefs
const { count, name } = toRefs(state)

常见响应式陷阱与解决方案

陷阱一:reactive对象整体替换

let state = reactive({ count: 0 })

// 错误:整体替换会断开响应式连接
state = reactive({ count: 1 })

// 正确:逐属性修改或使用Object.assign
Object.assign(state, { count: 1 })

整体替换赋值使变量指向新的Proxy对象,原有模板和effect中引用的仍是旧Proxy,不会触发更新。Object.assign在原对象上逐属性修改,能正常触发响应式。

陷阱二:reactive的局限性

reactive只能代理对象类型,无法代理基本类型。Map、Set的响应式操作通过Proxy拦截get返回特殊方法实现,直接调用原型方法(如map.set())能触发响应式,但通过解构获取方法后调用会失效:

const map = reactive(new Map())

// 正确
map.set('key', 'value')

// 错误:解构后set方法丢失响应式
const { set } = map
set('key', 'value')  // 不触发更新

陷阱三:标记非响应式的markRaw

某些场景下需要将对象排除响应式系统,如第三方类实例、大型不可变数据。markRaw标记对象使其永远不被转为Proxy:

import { reactive, markRaw } from 'vue'

const state = reactive({
    chart: markRaw(new EChartsInstance())  // 不追踪ECharts实例
})

shallowReactive只对第一层属性做响应式,深层属性不做递归代理。适合只关心顶层属性变化的场景,减少Proxy创建开销。

computed与watch的实现差异

computed返回一个ref,具有缓存特性:只有当依赖的响应式数据变化时才重新计算。watch显式监听响应式源的变化,可获取新旧值。

import { ref, computed, watch } from 'vue'

const firstName = ref('张')
const lastName = ref('三')

// computed缓存
const fullName = computed(() => firstName.value + lastName.value)
// 只在firstName或lastName变化时才重新计算

// watch监听
watch(fullName, (newVal, oldVal) => {
    console.log(`名字从${oldVal}变为${newVal}`)
}, { immediate: true })

// watch监听多个源
watch([firstName, lastName], ([newFirst, newLast]) => {
    console.log(`${newFirst}${newLast}`)
})

// watch监听reactive属性需用函数
const state = reactive({ count: 0 })
watch(() => state.count, (newVal) => {
    console.log(`count: ${newVal}`)
})

watch监听reactive对象的某个属性时,必须传入getter函数。直接watch(state.count)传入的是当前值的快照,不具响应式。watch的immediate选项设为true时立即执行回调,flush选项设为’post’在DOM更新后执行回调。

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

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

相关推荐