Vue2.0与Vue3.0响应式原理的差异

「这是我参与2022首次更文挑战的第26天,活动详情查看:2022首次更文挑战」。

前言

之前分了三篇文章对Vue3.0的响应式API以及源码进行了学习 响应式原理(一) 响应式原理(二) 响应式原理(三)

Vue3.0 的响应式底层是使用了 new Proxy() 对数据的 gettersetter 进行了拦截,过程中进行了依赖的收集,如果数据发生了变化,就会通知相应的依赖去做变化。如果了解过 Vue2.0 响应式原理应该就知道,Vue2.0的响应式底层是用 Object.defineProperty 进行实现的。为什么会有如此大的变化?这篇文章将通过学习 Vue2.0 和 Vue3.0 的源码比较两者在响应式实现上的差异。

响应式源码

Vue2.0项目地址 Vue3.0项目地址

MDN Object.defineProperty() Proxy

Vue2.0

  • 源码位置 \src\core\observer\index.js
  • 响应式函数 defineReactive,初始化时需要循环频繁调用,因为 Object.defineProperty 每次只能对对象中的一个属性进行拦截操作,并且如果当前属性的值 val 也是一个对象就会调用一个观察者函数 observe(val),为对象创建观察者
export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
  // 创建依赖
  const dep = new Dep()
  ...
  // 如果属性还是对象,递归observe
  let childOb = !shallow && observe(val)
  // 数据拦截操作
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val
      if (Dep.target) {
        // 收集依赖
        dep.depend()
        if (childOb) { 
          //递归收集依赖
          childOb.dep.depend()
          ...
        }
      }
      return value
    },
    set: function reactiveSetter (newVal) {
      ...
      // 通知依赖更新
      dep.notify()
    }
  })
}
复制代码
  • 观察者函数 observe 中会创建并返回一个 obObserver 类的实例
export function observe (value: any, asRootData: ?boolean): Observer | void {
  if (!isObject(value) || value instanceof VNode) {
    return
  }
  let ob: Observer | void
  ...
    // 观察者对象
    ob = new Observer(value)
  ...
  return ob
}
复制代码
  • Observer 类会执行其中的 walk 方法,walk 会对当前对象进行遍历并继续调用 defineReactive 这样就形成了一个递归调用
export class Observer {
  value: any;
  dep: Dep;
  vmCount: number; // number of vms that have this object as root $data

  constructor (value: any) {
    this.value = value
    this.dep = new Dep()
    this.vmCount = 0
    def(value, '__ob__', this)
    if (Array.isArray(value)) {
      // 数组
      ...
    } else {
      // 如果是对象
      this.walk(value)
    }
  }

  /**
   * Walk through all properties and convert them into
   * getter/setters. This method should only be called when
   * value type is Object.
   */
  walk (obj: Object) {
    const keys = Object.keys(obj)
    // 循环当前对象进行响应式拦截
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i])
    }
  }
  ...
}
复制代码
  • 到这里就看出了使用 Object.defineProperty 方式进行响应式操作的一些缺点

    • 每次只能监控一个key
    • 初始化时需要循环递归遍历obj中的所有key,速度慢,并且会产生闭包,资源占用比较大
    • 不能对 Collection 类型(set map)的数据进行响应式的监听

Vue2对于 Object.defineProperty 一些先天性的缺陷做了一些兼容操作

  • 无法检测动态属性新增和删除,新增了 setdel 方法
export function set (target: Array<any> | Object, key: any, val: any): any {
  ...
  // 对新增属性重新定义响应式
  defineReactive(ob.value, key, val)
  // 通知依赖更新
  ob.dep.notify()
  return val
}
export function del (target: Array<any> | Object, key: any) {
  ...
  // 手动删除属性
  delete target[key]
  if (!ob) {
    return
  }
  // 通知依赖更
  ob.dep.notify()
}
复制代码
  • 无法检测调用 Array 上的方法对数组进行的改变,比如 push pop 等,Vue2通过对 Array.prototype 数组原型上的方法进行重写实现响应式拦截(但是对 数组下标 的操作还是无法监听到,还是需要使用 set
const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)

const methodsToPatch = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]

// 拦截原型上的方法并派发事件
methodsToPatch.forEach(function (method) {
  // cache original method
  const original = arrayProto[method]
  def(arrayMethods, method, function mutator (...args) {
    const result = original.apply(this, args)
    const ob = this.__ob__
    let inserted
    switch (method) {
      case 'push':
      case 'unshift':
        inserted = args
        break
      case 'splice':
        inserted = args.slice(2)
        break
    }
    if (inserted) ob.observeArray(inserted)
    // 通知依赖更新
    ob.dep.notify()
    return result
  })
})
...
// Observer类
export class Observer {
  value: any;
  dep: Dep;
  vmCount: number; // number of vms that have this object as root $data

  constructor (value: any) {
    this.value = value
    this.dep = new Dep()
    this.vmCount = 0
    def(value, '__ob__', this)
    if (Array.isArray(value)) {
      // 包装之前重写的数组原型
      if (hasProto) {
        protoAugment(value, arrayMethods)
      } else {
        copyAugment(value, arrayMethods, arrayKeys)
      }
      // 给数组元素添加响应式
      this.observeArray(value)
    } else {
      this.walk(value)
    }
  }

  ...

  // 给数组元素添加响应式
  observeArray (items: Array<any>) {
    for (let i = 0, l = items.length; i < l; i++) {
      observe(items[i])
    }
  }
}
复制代码

Vue3.0

由于 Object.defineProperty 存在的这些缺点,Vue3.0使用了 Proxy 去做响应式,自然就解决了这些问题,但是 Proxy 不支持IE11及以下版本

  • 在之前的文章 Vue3.0源码学习——响应式原理(一) 中学习了,如果在响应式API reactive 中传入的是普通对象,最终使用的是一个 Proxy,并在对应的 getsetdeletePropertyhasownKeys 中进行响应式的拦截操作
function createReactiveObject(
  target: Target,
  isReadonly: boolean,
  baseHandlers: ProxyHandler<any>,
  collectionHandlers: ProxyHandler<any>,
  proxyMap: WeakMap<Target, any>
) {
  ...
  
  // 创建代理对象,将传入的原始对象作为代理目标
  const proxy = new Proxy(
    target,
    // 如果代理的是普通对象,handler使用的是baseHandlers,就是之前传入的mutableHandlers
    targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
  )
  proxyMap.set(target, proxy)
  return proxy
}
...
// handler
export const mutableHandlers: ProxyHandler<object> = {
  get,
  set,
  deleteProperty,
  has,
  ownKeys
}
复制代码

小结

  • 实现方式
    • Vue2:Object.defineProperty(obj, key, {})
    • Vue3:new Proxy(obj, {})
  • Object.definePropertyProxy 的优缺点
    • defineProperty方式每次只能监控一个key,初始化时需要循环递归遍历obj中的所有key,速度慢,资源占用大,闭包
    • defineProperty无法检测动态属性新增和删除
    • defineProperty无法很好的支持数组,需要额外的数组响应式实现
    • Vue2无法支持Collection类型:set、map
    • Proxy不支持IE11及以下版本

猜你喜欢

转载自juejin.im/post/7067912794808516638