对比vue2和vue3的响应式原理

2,125 阅读3分钟

总所周知,vue2的响应式原理是靠Object.defineProperty实现。vue3却是通过Proxy来实现。那么,它们之间有什么区别呢?这篇文章将通过手撕简易原理来认识其实现原理。

vue2

先上源码

    function trackArray(arr) {
      const prototype = Array.prototype
      const newProto = Object.create(prototype)
      const methods = ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse']
      methods.forEach(item => {
        newProto[item] = function (...args) {
          prototype[item].call(arr, ...args)
          console.log('call ', item)
        }
      })
      arr.__proto__ = newProto
    }

    function track(obj, key, value) {
      observe(value) // 需要在此处深度监听
      Object.defineProperty(obj, key, {
        get() {
          console.log('get', key)
          return value
        },
        set(newValue) {
          if (value === newValue) {
            return
          }
          console.log('set', key)
          value = observe(newValue) // 当修改value为对象时, 需要再进一步监听
        }
      })
    }

    function observe(obj) {
      if (typeof obj !== 'object' || obj === null) { // 只监听引用类型数据
        return obj
      }
      
      if (Array.isArray(obj)) {
        trackArray(obj)
        return obj
      }

      for (let key in obj) {
        track(obj, key, obj[key])
      }

      return obj
    }

因为 Object.defineProperty是属性层级的api。只能拦截某对象的某个属性.所以只能遍历需要监听对象的属性来依次跟踪。所以监听不了新增属性。而Object.definePropertyapi本身无法跟踪对象删除属性。 所以在vue2中,官方提供了$set$delete

值得一提的是,实现深度监听是一次性递归所有层级,即使还没有使用。

而且对于数组需要额外处理。不然无法跟踪其原型链上的数组api。数组处理思想是:创建一个指向Array.prototype的对象,在这个对象中创建vue2中的那7个api属性。其每个属性都是调用Array.prototype上的api.同时可实现跟踪。注意this指向。最后,将监听的数组使其原型指向这个创建的对象。

几个问题:
1、监听数组时,可以直接在Array.prototype上做修改吗?为什么?

2、track(obj, key, obj[key])调用track函数时,为什么要传递第三个参数(obj[key])?

3、源码中,下面展示的代码可以交换吗?

      //旧
      methods.forEach(item => {
        newProto[item] = function (...args) {
          prototype[item].call(arr, ...args)
          console.log('call ', item)
        }
      })
      //新
      methods.forEach(item => {
        newProto[item] = (...args) => {
          prototype[item].call(this, ...args) // arr换成this
          console.log('call ', item)
        }
      })

4、源码中对应的下面代码,深度递归监听observe(value)放在get()的返回值上行不行?反正在set(value)中也存在深度递归监听,不必放在开始?
答:若是放在返回值中,那么在访问引用类型时都会递归到底,从而出现多余的深层次的访问跟踪。那么为什么vue3却没有这种情况呢? proxy只是相当于包裹一层代理,并不会直接访问属性

      ...
      observe(value) // 需要在此处深度监听
      Object.defineProperty(obj, key, {
        get() {
          console.log('get', key)
          return value // return observe(value)?
        },
      ...

vue3

老规矩,先上源码

    function reactive(obj) {
      if (typeof obj !== 'object' || obj === null) {
        return obj
      }

      const proxyHandle = {
        get(target, key, receiver) {
          const result = Reflect.get(target, key, receiver)
          if (Reflect.ownKeys(target).includes(key)) {
            console.log('get props: ', key)
          }
          const obervedRes = reactive(result)
          return obervedRes
        },
        
        set(target, key, value, receiver) {
          if (value === target[key]) {
            return true
          }
          const oldKeys = Reflect.ownKeys(target)
          if (oldKeys.includes(key)) {
            console.log('set: has props: ', key)
          } else {
            console.log('set: new Props: ', key)
          }     
          
          // const observedVal = reactive(value)
          const result = Reflect.set(target, key, value, receiver)
          return result
        },
        
        deleteProperty(target, key) {
          const res = Reflect.deleteProperty(target, key)
          console.log('delete: props')
          return res
        }
      }

      return new Proxy(obj, proxyHandle)
    }

proxy代理是针对对象层次的api。其实现相对也是挺简单的...

几个问题:
为什么在set值时不用深度监听?

总结

vue2中Object.defineProperty

  1. 深度监听在初始化时就一次性递归监听好了
  2. 无法跟踪到对象增加新值,删除属性
  3. 数组需要额外处理

vue3中proxy:

  1. 深度监听是在获取值的时候才对其监听(按需监听)
  2. 可以很完备的监听到属性增加和删除,
  3. 数组可以原生使用api,不需要再对需要监听的api再做处理