Skip to content

深入响应式

Vue 中的响应性是如何工作的

在 JavaScript 中有两种劫持 property 访问的方式:getter / settersProxy

  • Vue 2 使用 getter / setters 完全是出于支持旧版本浏览器的限制。
  • Vue 3 中则使用了 Proxy 来创建响应式对象,仅将 getter / setter 用于 ref。

伪代码:

js
function reactive(obj) {
  return new Proxy(obj, {
    get(target, key) {
      track(target, key)
      return target[key]
    },
    set(target, key, value) {
      target[key] = value
      trigger(target, key)
    },
  })
}

function ref(value) {
  const refObject = {
    get value() {
      track(refObject, 'value')
      return value
    },
    set value(newValue) {
      value = newValue
      trigger(refObject, 'value')
    },
  }
  return refObject
}

依赖收集机制

依赖收集(Dependency Collection)是 Vue 响应式系统的核心机制。

Vue 2 依赖收集

Vue 2 依赖收集 通过 Object.defineProperty 劫持属性,基于 DepWatcher 进行依赖管理,但存在新增属性、数组索引、深层对象性能等问题。

基于 Object.defineProperty

Vue 2 使用 Object.defineProperty() 劫持数据对象的 getter 和 setter,在访问属性时收集依赖,在修改属性时触发更新

依赖收集流程

关键角色
  • Dep(依赖管理类):存储所有订阅当前属性的 Watcher 实例,并在数据变更时通知它们更新。
  • Watcher(观察者):表示组件或计算属性,每个 Watcher 订阅多个 Dep,当 Dep 触发更新时,Watcher 重新计算或触发渲染。
依赖收集过程
  1. 访问数据时(getter 触发)

    • getter 触发时,当前正在执行的 Watcher(例如组件渲染)会被存入 Dep
    • DepWatcher 存入 subs(订阅者列表)。
  2. 数据更新时(setter 触发)

    • setter 修改数据时,会通知 Dep,触发 subs 中的所有 Watcher 进行更新。
示例代码
js
class Dep {
  constructor() {
    this.subs = []
  }
  depend() {
    if (Dep.target) {
      this.subs.push(Dep.target) // 依赖收集
    }
  }
  notify() {
    this.subs.forEach(watcher => watcher.update()) // 触发更新
  }
}
Dep.target = null // 当前 Watcher

function defineReactive(obj, key, val) {
  const dep = new Dep()
  Object.defineProperty(obj, key, {
    get() {
      dep.depend() // 依赖收集
      return val
    },
    set(newVal) {
      val = newVal
      dep.notify() // 触发更新
    },
  })
}

const data = {}
defineReactive(data, 'msg', 'hello')

function updateComponent() {
  console.log('组件更新', data.msg)
}

// 创建 Watcher
Dep.target = { update: updateComponent }
console.log(data.msg) // 触发 getter,收集依赖
Dep.target = null // 依赖收集结束

data.msg = 'world' // 触发 setter,通知 Watcher 更新

✅ Vue 2 依赖收集基于 Object.defineProperty,但它有一些缺陷

  • 无法监听数组索引的变化(只能监听 pushpop 等变更方法)。
  • 无法动态添加新属性(新增属性不是响应式的,需要 Vue.set())。
  • 深层嵌套对象依赖收集成本高(需要递归遍历整个对象)。

Vue 3 依赖收集

Vue 3 依赖收集 基于 Proxy 实现 track()/trigger(),更灵活,性能更优,能自动追踪属性的新增和删除,适用于复杂的响应式需求。

基于 Proxy

Vue 3 通过 Proxy 代理整个对象,不再使用 Object.defineProperty,可以直接监听新增属性和删除属性的变化

依赖收集流程

关键角色
  • ReactiveEffect(响应式副作用):类似 Vue 2 的 Watcher,负责执行副作用(组件渲染、计算属性等)。
  • targetMap(存储依赖的全局 Map)
    • targetMap 维护所有响应式对象的依赖信息。
    • targetMap.get(obj) 得到 objdepsMap(存储对象 obj 内各个属性的 ReactiveEffect)。
    • depsMap.get(key) 得到 key 具体的 Set<ReactiveEffect>,即依赖于 key 的所有 ReactiveEffect
依赖收集过程
  1. 访问数据时(getter 触发)

    • activeEffect 是当前正在运行的副作用(如 setup()computed)。
    • track(target, key) 记录 activeEffecttargetMap,实现依赖收集。
  2. 数据更新时(setter 触发)

    • trigger(target, key) 遍历 targetMap[target][key] 中的 ReactiveEffect,触发更新。
示例代码
js
const targetMap = new WeakMap()
let activeEffect = null

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) {
    dep.forEach(effect => effect())
  }
}

// 代理对象
function reactive(obj) {
  return new Proxy(obj, {
    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
    },
  })
}

// 测试
const state = reactive({ count: 0 })

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

effect(() => {
  console.log('组件更新', state.count)
})

state.count = 1 // 触发更新

Vue 3 依赖收集基于 Proxy,相比 Vue 2 具有明显优势:

  • 支持新增/删除属性,不需要 Vue.set()
  • 对数组的索引变更具有响应性
  • 优化了性能,避免 Vue 2 递归劫持所有属性的开销。

Vue 2 vs Vue 3 依赖收集对比总结

机制Vue 2Vue 3
响应式实现Object.definePropertyProxy
依赖管理Dep / WatcherReactiveEffect / targetMap
依赖收集getter 触发 dep.depend()getter 触发 track()
触发更新setter 触发 dep.notify()setter 触发 trigger()
新增属性需要 Vue.set()自动追踪
删除属性不能追踪自动追踪
数组索引不能追踪可以追踪
深度监听递归遍历所有属性访问时动态代理

基于 MIT 许可发布