从零实现一个迷你 Vue 响应式系统:Object.defineProperty 与 Proxy 对比

响应式系统是现代前端框架的基石。Vue 2 依靠 Object.defineProperty 实现数据劫持,而 Vue 3 则全面转向 Proxy。本文将从零实现一个迷你响应式系统,并深入对比这两种方案的优劣。

一、响应式系统的核心目标

一个响应式系统需要完成三件事:

  1. 数据劫持:拦截对数据的读取和写入。
  2. 依赖收集:在读取数据时,记录“谁”用到了这个数据。
  3. 派发更新:在数据变化时,通知所有依赖它的“人”重新执行。

下面我们用两种方式分别实现一个最小可用的版本。

二、基于 Object.defineProperty 的实现

2.1 核心代码

function observe(obj) {
  if (typeof obj !== 'object' || obj === null) return
  Object.keys(obj).forEach(key => {
    let value = obj[key]
    const dep = []
    Object.defineProperty(obj, key, {
      get() {
        if (window.__activeEffect) {
          dep.push(window.__activeEffect)
        }
        return value
      },
      set(newVal) {
        if (newVal === value) return
        value = newVal
        dep.forEach(fn => fn())
      }
    })
  })
}

2.2 使用示例

const state = { count: 0 }
observe(state)

function effect(fn) {
  window.__activeEffect = fn
  fn()
  window.__activeEffect = null
}

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

state.count++ // 输出: count is: 1

2.3 存在的问题

  • 无法监听新增/删除属性state.newKey = 1 不会触发更新,Vue 2 因此提供了 Vue.set / Vue.delete
  • 无法监听数组索引和 length 变化:Vue 2 通过重写数组的 7 个方法(push、pop 等)来绕过。
  • 初始化时需递归遍历所有属性:性能开销大,尤其是深层嵌套对象。
  • 每个属性都需要创建一个闭包和 dep 数组:内存占用较高。

三、基于 Proxy 的实现

3.1 核心代码

function reactive(obj) {
  const depMap = new Map()

  return new Proxy(obj, {
    get(target, key, receiver) {
      const value = Reflect.get(target, key, receiver)
      if (window.__activeEffect) {
        if (!depMap.has(key)) depMap.set(key, [])
        depMap.get(key).push(window.__activeEffect)
      }
      // 嵌套对象惰性代理
      if (typeof value === 'object' && value !== null) {
        return reactive(value)
      }
      return value
    },
    set(target, key, value, receiver) {
      const oldVal = target[key]
      const result = Reflect.set(target, key, value, receiver)
      if (oldVal !== value && depMap.has(key)) {
        depMap.get(key).forEach(fn => fn())
      }
      return result
    },
    deleteProperty(target, key) {
      const hadKey = key in target
      const result = Reflect.deleteProperty(target, key)
      if (hadKey && depMap.has(key)) {
        depMap.get(key).forEach(fn => fn())
      }
      return result
    }
  })
}

3.2 使用示例

const state = reactive({ count: 0 })

function effect(fn) {
  window.__activeEffect = fn
  fn()
  window.__activeEffect = null
}

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

state.count++        // 输出: count is: 1
state.newKey = 'hi'  // 新增属性也能触发(如果 effect 中读取过 newKey)
delete state.count   // 删除属性也能触发

3.3 优势

  • 支持新增/删除属性:通过 deletePropertyset 拦截。
  • 支持数组索引和 length 变化:Proxy 天然支持。
  • 惰性代理:只有访问到嵌套对象时才递归代理,初始化性能更好。
  • 代码更简洁:不需要为每个属性单独定义 getter/setter。

四、两种方案对比

维度 Object.defineProperty Proxy
监听新增属性 ❌ 不支持 ✅ 支持
监听删除属性 ❌ 不支持 ✅ 支持
监听数组索引 ❌ 不支持 ✅ 支持
监听数组 length ❌ 不支持 ✅ 支持
初始化性能 差(需递归遍历) 好(惰性代理)
内存占用 高(每个属性一个闭包)
兼容性 IE9+ 不支持 IE,需 polyfill
拦截能力 仅 get/set 13 种拦截操作

五、进一步完善迷你系统

真实的 Vue 响应式系统还需要处理:

  • 依赖清理:每次 effect 重新执行前清空旧依赖,避免无效触发。
  • 嵌套 effect:用栈管理 activeEffect
  • 调度器:将更新推入微任务队列,避免同步多次触发。
  • ref 与 computed:基于 effect 的派生值。
  • 只读与浅响应readonlyshallowReactive 等变体。

以依赖清理为例:

function effect(fn) {
  const wrapped = () => {
    cleanup(wrapped)
    window.__activeEffect = wrapped
    fn()
    window.__activeEffect = null
  }
  wrapped.deps = []
  wrapped()
  return wrapped
}

function cleanup(effectFn) {
  effectFn.deps.forEach(dep => {
    const i = dep.indexOf(effectFn)
    if (i > -1) dep.splice(i, 1)
  })
  effectFn.deps.length = 0
}

六、总结

Object.defineProperty 是 Vue 2 时代的选择,它在兼容性上有优势,但存在无法监听新增/删除属性、数组变异、初始化性能差等硬伤。Proxy 作为 ES6 的新特性,提供了更强大、更优雅的拦截能力,让 Vue 3 的响应式系统得以大幅简化并提升性能。

理解这两种方案的差异,不仅能帮助你更好地使用 Vue,也能在设计自己的状态管理库时做出更明智的技术选型。建议读者亲手实现一遍上述代码,感受从“数据劫持”到“依赖收集”再到“派发更新”的完整链路。

未经允许不得转载:任鹏个人博客 » 从零实现一个迷你 Vue 响应式系统:Object.defineProperty 与 Proxy 对比

赞 (0) 打赏

评论 0

取消
  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

微信扫一扫打赏