Vue3 响应式原理与优势

Proxy、Reflect 和懒代理的实现机制

问题

Vue3 的响应式系统是如何实现的?相比 Vue2 有哪些优势?

解答

Vue2 的问题

Vue2 使用 Object.defineProperty,存在以下限制:

// Vue2 无法检测的情况
const obj = { a: 1 }

// 1. 新增属性无法检测
obj.b = 2  // 不触发更新,需要 Vue.set()

// 2. 数组索引修改无法检测
arr[0] = 'new'  // 不触发更新

// 3. 初始化时必须递归遍历所有属性
function defineReactive(obj) {
  Object.keys(obj).forEach(key => {
    let value = obj[key]
    // 递归处理嵌套对象(性能开销大)
    if (typeof value === 'object') {
      defineReactive(value)
    }
    Object.defineProperty(obj, key, {
      get() { /* 收集依赖 */ return value },
      set(newVal) { /* 触发更新 */ value = newVal }
    })
  })
}

Vue3 使用 Proxy

function reactive(target) {
  return new Proxy(target, {
    get(target, key, receiver) {
      track(target, key)  // 收集依赖
      return Reflect.get(target, key, receiver)
    },
    set(target, key, value, receiver) {
      const result = Reflect.set(target, key, value, receiver)
      trigger(target, key)  // 触发更新
      return result
    },
    deleteProperty(target, key) {
      const result = Reflect.deleteProperty(target, key)
      trigger(target, key)
      return result
    }
  })
}

const state = reactive({ a: 1 })
state.b = 2      // ✅ 新增属性可检测
delete state.a   // ✅ 删除属性可检测

为什么用 Reflect

const obj = {
  _name: 'vue',
  get name() {
    return this._name
  }
}

const proxy = new Proxy(obj, {
  get(target, key, receiver) {
    console.log('get:', key)
    // 错误写法:target[key] 会导致 this 指向原对象
    // return target[key]
    
    // 正确写法:Reflect.get 保证 this 指向 proxy
    return Reflect.get(target, key, receiver)
  }
})

proxy.name
// 使用 Reflect:get: name, get: _name(两次拦截)
// 不用 Reflect:get: name(只拦截一次,_name 的访问丢失)

懒代理(惰性代理)

Vue3 不在初始化时递归代理所有嵌套对象,而是在访问时才代理:

const reactiveMap = new WeakMap()

function reactive(target) {
  // 已代理过的直接返回
  if (reactiveMap.has(target)) {
    return reactiveMap.get(target)
  }

  const proxy = new Proxy(target, {
    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 result = Reflect.set(target, key, value, receiver)
      trigger(target, key)
      return result
    }
  })

  reactiveMap.set(target, proxy)
  return proxy
}

// 示例
const state = reactive({
  user: {
    profile: {
      name: 'vue'
    }
  }
})

// 初始化时只代理最外层
// 访问 state.user 时才代理 user 对象
// 访问 state.user.profile 时才代理 profile 对象

简化的依赖收集与触发

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 deps = depsMap.get(key)
  if (!deps) {
    depsMap.set(key, (deps = new Set()))
  }
  
  deps.add(activeEffect)
}

// 触发更新
function trigger(target, key) {
  const depsMap = targetMap.get(target)
  if (!depsMap) return
  
  const deps = depsMap.get(key)
  if (deps) {
    deps.forEach(effect => effect())
  }
}

// effect 函数
function effect(fn) {
  activeEffect = fn
  fn()
  activeEffect = null
}

// 使用
const state = reactive({ count: 0 })

effect(() => {
  console.log('count:', state.count)
})

state.count++  // 自动打印 "count: 1"

关键点

  • Proxy 代理整个对象:可拦截新增、删除属性,支持数组索引修改
  • Reflect 保证正确的 this 指向:getter 中的 this 指向代理对象而非原对象
  • 懒代理提升性能:嵌套对象在访问时才代理,避免初始化时的递归开销
  • WeakMap 存储依赖:target → Map(key → Set(effects)),自动垃圾回收
  • 不支持 IE:Proxy 无法 polyfill,这是 Vue3 放弃 IE 的原因