Vue数据响应式底层原理

数据变了,视图自动更新——这背后是怎么做到的?


什么是数据响应式

当数据发生变化时,自动执行依赖该数据的相关函数(如渲染函数)。

const data = { count: 0 }

autorun(() => {
  document.body.innerHTML = data.count  // 用到了 count
})

data.count = 1  // → 自动触发上面的函数,页面更新

Vue 2:Object.defineProperty

核心思路:劫持属性的读写,读取时收集依赖,写入时派发更新。

function observe(obj) {
  for (const key in obj) {
    let internalValue = obj[key]
    let funcs = []  // 依赖该属性的所有函数

    Object.defineProperty(obj, key, {
      get() {
        // 依赖收集:记录谁在用我
        if (window.__func && !funcs.includes(window.__func)) {
          funcs.push(window.__func)
        }
        return internalValue
      },
      set(val) {
        internalValue = val
        // 派发更新:通知依赖我的函数重新执行
        funcs.forEach(fn => fn())
      }
    })
  }
}

function autorun(fn) {
  window.__func = fn  // 标记当前正在运行的函数
  fn()               // 执行(触发 get,完成依赖收集)
  window.__func = null
}

执行流程

第一步:依赖收集(读数据时)

autorun(fn)
  → window.__func = fn
  → fn() 执行,读取 data.count
  → 触发 count 的 get
  → fn 被记录进 funcs
  → window.__func = null

第二步:派发更新(写数据时)

data.count = 1
  → 触发 count 的 set
  → 遍历 funcs,重新执行所有函数
  → 视图更新

Object.defineProperty 的三个天生缺陷

这不是代码写得不好,是 Object.defineProperty 这个 API 本身的限制。

缺陷一:无法检测新增属性

observe(data)
data.newKey = 'hello'  // ❌ 新增的属性没有 getter/setter,完全不响应

根本原因observe 只能对已存在的属性做劫持,对后来新增的属性无能为力。

这就是 Vue 2 中需要 Vue.set(obj, key, value) 的原因——它会手动触发响应。


缺陷二:无法检测数组下标变更

observe(data)
data.list[0] = 'new'    // ❌ 无法触发更新
data.list.length = 0    // ❌ 无法触发更新

Vue 2 的解决方案:重写数组的 7 个变异方法(push、pop、shift、unshift、splice、sort、reverse),让它们在调用时手动触发更新。

所以 Vue 2 中这样写才有响应:

data.list.push('new')   // ✅ 走的是重写后的 push,会触发更新
data.list.splice(0, 1)  // ✅ 同上

缺陷三:嵌套对象需要递归处理

const data = { user: { name: 'Alice' } }
observe(data)

data.user.name = 'Bob'  // ❌ user 内部没有被 observe,不响应

Vue 2 的解决方案:初始化时递归遍历所有嵌套对象,全部做 defineProperty

带来的性能问题:数据层级越深,初始化越慢——无论你用不用深层数据,都会全部遍历一遍。


Vue 3:Proxy 全面升级

Vue 3 用 Proxy 替换 Object.defineProperty,从根本上解决了上面三个问题。

function reactive(obj) {
  return new Proxy(obj, {
    get(target, key) {
      track(target, key)   // 依赖收集
      const val = target[key]
      return typeof val === 'object' ? reactive(val) : val  // 惰性递归
    },
    set(target, key, value) {
      target[key] = value
      trigger(target, key) // 派发更新
      return true
    }
  })
}

为什么 Proxy 能解决这些问题?

  • 新增属性:Proxy 代理的是整个对象,任何属性的读写都会被拦截,包括新增的
  • 数组下标arr[0] = 'new' 也是属性写入,Proxy 的 set 直接捕获
  • 嵌套对象:不需要初始化时全量递归,访问到某个属性时再代理(惰性),性能更好

Vue 2 vs Vue 3 对比

Vue 2(defineProperty) Vue 3(Proxy)
新增属性 ❌ 不响应,需 Vue.set ✅ 自动响应
数组下标 ❌ 不响应,需重写方法 ✅ 自动响应
嵌套对象 ⚠️ 初始化全量递归,性能差 ✅ 访问时按需代理,性能好
浏览器兼容 ✅ IE9+ ❌ 不支持 IE

一句话总结

Vue 响应式的本质是拦截数据的读(收集依赖)写(派发更新)。Vue 2 用 Object.defineProperty 实现,存在新增属性、数组下标、性能三个天生缺陷;Vue 3 改用 Proxy,一次性全部解决。

posted @ 2023-02-19 22:00  HuangBingQuan  阅读(104)  评论(0)    收藏  举报