Vue2 响应式原理

理解 Object.defineProperty、依赖收集和 Watcher 的工作机制

问题

Vue2 是如何实现数据响应式的?请解释 Object.defineProperty、依赖收集和 Watcher 的作用。

解答

整体流程

  1. Observer:遍历对象,用 Object.defineProperty 劫持属性的 getter/setter
  2. Dep:每个属性对应一个 Dep,用于收集依赖(Watcher)
  3. Watcher:订阅者,数据变化时执行回调

简化实现

// 依赖收集器
class Dep {
  constructor() {
    this.subs = [] // 存储 watcher
  }

  addSub(watcher) {
    this.subs.push(watcher)
  }

  // 通知所有 watcher 更新
  notify() {
    this.subs.forEach(watcher => watcher.update())
  }
}

// 当前正在计算的 watcher
Dep.target = null

// 观察者
class Watcher {
  constructor(vm, key, callback) {
    this.vm = vm
    this.key = key
    this.callback = callback

    // 触发 getter,收集依赖
    Dep.target = this
    this.value = vm[key]
    Dep.target = null
  }

  update() {
    const newValue = this.vm[this.key]
    if (newValue !== this.value) {
      this.value = newValue
      this.callback(newValue)
    }
  }
}

// 将对象变成响应式
function observe(obj) {
  if (typeof obj !== 'object' || obj === null) return

  Object.keys(obj).forEach(key => {
    defineReactive(obj, key, obj[key])
  })
}

// 定义响应式属性
function defineReactive(obj, key, value) {
  const dep = new Dep()

  // 递归处理嵌套对象
  observe(value)

  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,

    get() {
      // 收集依赖
      if (Dep.target) {
        dep.addSub(Dep.target)
      }
      return value
    },

    set(newValue) {
      if (newValue === value) return
      value = newValue
      // 新值也要变成响应式
      observe(newValue)
      // 通知更新
      dep.notify()
    }
  })
}

使用示例

const data = { name: 'Vue', count: 0 }

// 让 data 变成响应式
observe(data)

// 创建 watcher,监听 count 变化
new Watcher(data, 'count', (newValue) => {
  console.log('count 变化了:', newValue)
})

data.count = 1 // 输出: count 变化了: 1
data.count = 2 // 输出: count 变化了: 2

依赖收集过程

1. new Watcher() 时,设置 Dep.target = 当前 watcher
2. 读取 vm[key],触发 getter
3. getter 中检测到 Dep.target,将 watcher 加入 dep.subs
4. 清空 Dep.target

数据变化时:
1. 触发 setter
2. 调用 dep.notify()
3. 遍历 subs,执行每个 watcher.update()

关键点

  • Object.defineProperty 只能劫持已存在的属性,无法检测属性的新增和删除
  • Dep 是发布者,每个响应式属性都有一个 Dep 实例
  • Watcher 是订阅者,在初始化时触发 getter 完成依赖收集
  • Dep.target 是全局变量,用于在 getter 中识别当前 watcher
  • 数组需要特殊处理,Vue2 通过重写数组方法(push、pop 等)实现响应式