FumadocsZDecode
Vue

响应式原理

一、Vue 2 — Object.defineProperty

核心:遍历对象每个 key,用 defineProperty 劫持 getter / setter。

function defineReactive(obj, key, val) {
  const dep = new Dep() // 每个 key 一个依赖收集器

  Object.defineProperty(obj, key, {
    get() {
      if (Dep.target) dep.depend() // 收集依赖(谁在用我)
      return val
    },
    set(newVal) {
      if (newVal === val) return
      val = newVal
      dep.notify() // 通知更新(告诉用我的人:我变了)
    },
  })
}

// 初始化时递归遍历所有 key
// Object.keys 只返回可枚举属性,不可枚举的 key 不会被劫持
function observe(data) {
  Object.keys(data).forEach((key) => {
    defineReactive(data, key, data[key])
  })
}

缺陷

// 新增属性 → 检测不到(初始化时没有这个 key,没挂 getter/setter)
this.obj.newKey = 'hello' // ❌ 不触发更新

// 删除属性 → 检测不到
delete this.obj.name // ❌ 不触发更新

// 数组下标直接赋值 → 检测不到
this.arr[0] = 'new' // ❌ 不触发更新

// 修改数组长度 → 检测不到
this.arr.length = 0 // ❌ 不触发更新

二、$set 解决了什么

专门解决"新增属性检测不到"和"数组下标赋值检测不到"两个问题。

this.$set(this.obj, 'newKey', 'hello') // ✅ 触发更新
this.$set(this.arr, 0, 'new') // ✅ 触发更新

内部实现(简化)

function set(target, key, val) {
  // 数组:走 splice(Vue 重写过 splice,能触发更新)
  if (Array.isArray(target)) {
    target.splice(key, 1, val)
    return val
  }

  // 对象已有的 key:已经有 getter/setter,直接赋值
  if (key in target) {
    target[key] = val
    return val
  }

  // 对象新增 key:手动补上 defineReactive,再手动通知更新
  defineReactive(target, key, val)
  target.__ob__.dep.notify()
  return val
}

本质:手动给新属性补 defineProperty,再手动通知依赖。


三、Vue 3 — Proxy

核心:用 Proxy 代理整个对象,不再逐个 key 劫持。

function reactive(target) {
  return new Proxy(target, {
    get(target, key, receiver) {
      track(target, key) // 收集依赖
      const result = Reflect.get(target, key, receiver)
      if (isObject(result)) return reactive(result) // 惰性递归
      return result
    },
    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
    },
  })
}

不再需要 $set

const state = reactive({ name: 'Tom' })

state.age = 18 // ✅ 新增属性,Proxy set 陷阱自动捕获
delete state.name // ✅ 删除属性,deleteProperty 陷阱自动捕获

const arr = reactive([1, 2, 3])
arr[0] = 99 // ✅ 触发更新
arr.length = 1 // ✅ 触发更新

根本原因Proxy 拦截的是对象级别的操作,新增、删除、任意 key 都经过代理,天然全覆盖。defineProperty 拦截的是已知 key 级别,初始化之后新增的 key 完全不感知。


四、对比总结

Vue 2Vue 3
底层 APIObject.definePropertyProxy
劫持粒度逐个 key整个对象
新增属性❌ 需要 $set✅ 自动检测
删除属性❌ 需要 $delete✅ 自动检测
数组下标❌ 需要 $set✅ 自动检测
初始化性能递归遍历所有 key,性能差惰性代理,访问时才递归,性能好
兼容性IE9+不支持 IE(Proxy 无法 polyfill)

五、一句话总结

Vue 2 用 defineProperty 按 key 劫持,新增 / 删除属性拦截不到,需要 $set 手动补响应式。Vue 3 用 Proxy 代理整个对象,任何操作都能拦截,$set 自然就不需要了。

On this page