Vue3 响应式原理深度剖析(Proxy + 依赖收集)

2026-03-20
Vue

“Vue3 的响应式是怎么实现的?”是面试必问题。本文不满足于”用了 Proxy”这个答案,而是带你从零手写一个迷你响应式系统,真正理解 reactiverefcomputed 的底层原理。

一、为什么 Vue3 从 defineProperty 换成 Proxy

Vue2 用 Object.defineProperty 劫持对象的已有属性,存在几个硬伤:

  • 无法检测新增/删除属性:需要 Vue.set / Vue.delete 补救;
  • 数组索引/长度变化无法响应:需要重写数组方法(push/pop 等 7 个);
  • 必须递归遍历:对象嵌套越深,初始化越慢。

Vue3 改用 Proxy 代理整个对象,能拦截所有操作(读取、赋值、删除、infor...in、函数调用),且是惰性代理——访问到深层时才递归代理,性能更好。

二、Proxy 与 Reflect

Proxy 可以拦截对象的底层操作,Reflect 提供了与之一一对应的静态方法:

const target = { name: 'shanky' }

const proxy = new Proxy(target, {
  get(obj, key, receiver) {
    console.log('读取', key)
    return Reflect.get(obj, key, receiver)  // 等价于 obj[key]
  },
  set(obj, key, value, receiver) {
    console.log('设置', key, value)
    return Reflect.set(obj, key, value, receiver)  // 返回是否成功
  },
})

Reflect.set/get 的第三个参数 receiverthis 的指向,直接使用 obj[key] 在遇到 getter/setter 时可能 this 指向错误,所以规范实现都用 Reflect

三、核心三要素:依赖收集、触发更新、副作用

响应式系统围绕三个概念:

  • 副作用函数(effect):会读取响应式数据、且希望数据变化时重新执行的函数(渲染函数、computed 等)。
  • 依赖收集(track):副作用读取数据时,把”数据 → 副作用”的映射记录下来。
  • 触发更新(trigger):数据变化时,找到所有依赖它的副作用并重新执行。

先建一个全局”当前正在执行的副作用”桶:

// 用一个栈保存正在执行的 effect,便于嵌套
let activeEffect
const effectStack = []

function effect(fn) {
  const eff = () => {
    // 入栈,清空依赖,重新收集
    effectStack.push(eff)
    activeEffect = eff
    cleanup(eff)
    fn()
    effectStack.pop()
    activeEffect = effectStack[effectStack.length - 1]
  }
  eff.deps = new Set()   // 记录本 effect 依赖了哪些"桶"
  eff()
  return eff
}

四、用 WeakMap 组织依赖关系

数据结构和依赖关系如下:

WeakMap  target -> Map  key -> Set<effect>
  • WeakMap:以原始对象为 key,WeakMap 的好处是对象被回收时,对应条目自动清除,避免内存泄漏。
  • Map:key 是对象的属性名。
  • Set:存放依赖该属性的所有 effect。
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)
  activeEffect.deps.add(dep)
}

function trigger(target, key) {
  const depsMap = targetMap.get(target)
  if (!depsMap) return
  const dep = depsMap.get(key)
  if (dep) {
    // 拷贝一份再遍历,避免执行过程中增删影响
    [...dep].forEach((eff) => eff())
  }
}

五、reactive:Proxy 的核心实现

function reactive(target) {
  return new Proxy(target, {
    get(obj, key, receiver) {
      track(obj, key)                       // 读取时收集依赖
      const res = Reflect.get(obj, key, receiver)
      // 惰性代理:如果值是对象,递归代理(注意缓存避免重复代理)
      if (isObject(res)) return reactive(res)
      return res
    },
    set(obj, key, value, receiver) {
      const hadKey = Object.prototype.hasOwnProperty.call(obj, key)
      const oldValue = obj[key]
      const ok = Reflect.set(obj, key, value, receiver)
      if (!ok) return false
      if (!hadKey) {
        trigger(obj, key, 'ADD')            // 新增属性也能响应(Proxy 的优势)
      } else if (oldValue !== value) {
        trigger(obj, key, 'SET')            // 值真正变化才触发
      }
      return ok
    },
    deleteProperty(obj, key) {
      const hadKey = Object.prototype.hasOwnProperty.call(obj, key)
      const ok = Reflect.deleteProperty(obj, key)
      if (ok && hadKey) trigger(obj, key, 'DELETE')
      return ok
    },
  })
}

六、ref:为什么还需要它

reactive 只能代理对象,而基础类型(number/string)没有属性可代理。于是 ref 用一个 { value } 包装对象:

function ref(val) {
  const wrapper = { value: val }
  // 复用 reactive 的代理逻辑(对包装对象做响应式)
  return reactive(wrapper)
}

在模板中 ref 会自动解包(8 而不是 ``),但在 JS 中必须 .value

顺带一提:Vue3 里 toRef / toRefs 用于把 reactive 的某个属性变成 ref,在 reactivesetup 解构、v-model 绑定中非常常用。

七、computed 的实现思路

computed = 惰性 effect + 缓存:只有在依赖变化且被读取时才重新计算。

function computed(getter) {
  let value
  let dirty = true   // 脏标记:是否缓存失效

  const effectFn = effect(() => {
    value = getter()       // 依赖变化时执行
    dirty = true           // 标记需要重新计算
  })

  return {
    get value() {
      if (dirty) {
        value = getter()
        dirty = false
      }
      return value
    },
  }
}

八、总结与面试答题话术

面试被问”Vue3 响应式原理”,一个清晰的回答框架:

  1. Vue3 用 Proxy 替代 defineProperty:能监听新增/删除/数组索引,惰性递归,解决 Vue2 三大痛点;
  2. 核心是依赖收集 + 触发更新track 收集”属性 → effect”的映射,trigger 在变化时执行相关 effect;
  3. 数据结构是 WeakMap→Map→Set 三层映射;
  4. reactive 针对对象,ref 针对基础类型,computed 是带缓存的惰性 effect;
  5. 能现场写出迷你实现,就是真正的理解而非背书。

掌握这套原理,你不仅能应对面试,也能更自信地阅读 Vue 源码、排查响应式相关的诡异 bug。


Similar Posts

Content