Vue 响应式原理

Vue 是怎么"知道"数据变了,然后更新页面的?

Vue 2:Object.defineProperty

Vue 2 遍历 data 对象的每个属性,用 Object.defineProperty 转换成 getter/setter:

function defineReactive(obj, key, val) {
  // 每个属性一个dep
  const dep = new Dep()

  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get() {
      // 依赖收集:谁读取了这个值
      dep.depend()
      return val
    },
    set(newVal) {
      if (newVal === val) return
      val = newVal
      // 触发更新:通知所有依赖更新
      dep.notify()
    }
  })
}

缺点

  1. 无法监听新增/删除属性:必须用 Vue.set / Vue.delete
  2. 无法监听数组下标arr[0] = 1 监听不到
  3. 性能差:初始化要递归遍历整个对象,深层嵌套很慢

Vue 3:Proxy

Vue 3 用 ES6 Proxy 拦截整个对象操作:

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)
      // 触发更新
      trigger(target, key)
      return result
    },
    deleteProperty(target, key) {
      const result = Reflect.deleteProperty(target, key)
      trigger(target, key)
      return result
    }
  })
}
  • 能力强:能拦截 Get/Set/Delete/Has 等 13 种操作
  • 惰性代理:只访问到的层级才代理,初始化超快

为什么 Vue 3 还需要 ref?

Proxy 只能代理对象,不能代理基本类型。

// 基本类型无法被代理
const count = 0
const proxy = new Proxy({}, {}) // 这样根本没用

// ref 的实现原理:包装成对象
function ref(value) {
  const refObj = {
    get value() {
      track(refObj, 'value')
      return value
    },
    set value(newVal) {
      value = newVal
      trigger(refObj, 'value')
    }
  }
  return refObj
}

// 使用
const count = ref(0)
count.value // 触发 get,收集依赖
count.value = 1 // 触发 set,触发更新

总结

Vue 2Vue 3
原理Object.definePropertyProxy
数组下标监听不到支持
新增属性需用 Vue.set自动支持
性能递归遍历慢惰性代理快