Vue响应式系统技术原理和Vue3响应式系统的优点

2,085 阅读14分钟

原文链接:www.yuque.com/wuhaosky/vu…


一 前言

数据驱动视图是MVVM框架的显著特点,MVVM框架的出现将前端开发者从繁杂的、“鸟巢”般的dom操作中解放出来,开发体验比jQuery/underscore模板提升了不知道几个层次。

想要实现数据驱动视图,需要解决两个问题,一是框架要知道数据什么时候变更、二是框架如何把变更后的数据更新到视图。对于解决第一个问题,React是通过开发者手动执行this.setState方法实现的,Vue框架是通过自身数据响应式系统实现的;对于解决第二个问题,React和Vue都是通过patch函数和virtual dom diff算法实现的。

这篇文章里,我们只关注Vue的响应式系统是如何实现的。无论是Vue2还是Vue3,两者的响应式系统都是基于观察者模式(发布订阅模式)实现的,所以都涉及这么几个概念:目标对象(target)、依赖收集器(Dep)、观察者(Watcher)。依赖收集器收集目标对象的观察者,当目标对象的状态发生改变,所有的观察者都将得到通知。示意图如下:

image.png


二 Vue2响应式系统的实现

我们先整体看下Vue2响应式系统是怎么运作的,有个大体的概念,然后再拆分每一部分,看下每部分的实现。

2.1 先整体看下Vue2响应式系统的实现

目标对象经过observe函数,新增__ob__属性,这个属性是一个Observer实例,这个Observer实例含有dep属性,dep属性指向依赖收集者。然后,对目标对象的每一个属性执行defineReactive函数,将属性转换成访问器属性,这样我们就可以对属性的读写操作进行拦截。这个过程称之为“数据劫持”。

Kapture 2019-12-12 at 11.38.26.gif


当执行观察者get方法时,会触发目标对象属性的getter方法,在getter方法里收集观察者,这个过程就是“收集观察者”。

当目标对象属性变更时,会触发目标对象的setter方法,在setter方法里执行观察者的update方法,这个过程就是“通知观察者”。

Kapture 2019-12-12 at 11.41.38.gif


2.2 observe函数和defineReactive函数

observe函数和defineReactive函数的作用是把目标对象属性转换成访问器属性

我们看下,这两个函数是怎么实现的。首先看下observe函数,observe函数创建一个Observer实例,在Observer构造函数里做了三件事:

1.首先new了一个依赖收集器,这个dep的作用是,当目标对象增删属性时,通知对目标对象“感兴趣”的观察者;

2.给目标对象添加不可枚举的__ob__属性,指向Observer实例;

3.最后遍历对象属性,并执行defineReactive函数。

export function observe (value: any): Observer | void {
  let ob: Observer | void
  ob = new Observer(value)
  return ob
}
export class Observer {
  value: any;
  dep: Dep;
  constructor (value: any) {
    this.value = value
    this.dep = new Dep()
    def(value, '__ob__', this)
    if (Array.isArray(value)) {
      // ...
    } else {
      this.walk(value)
    }
  }
  walk (obj: Object) {
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i])
    }
  }
}


js对象里目前存在的属性描述符有两种主要形式:数据描述符和访问器描述符。defineReactive函数的作用,就是把目标对象属性设置为访问器属性,这样可以在getter/setter方法中拦截属性的读写操作。如果属性是对象或数组,则递归执行observe函数,使目标对象深度可侦测。defineReactive函数里做了三件事:

1.创建了一个dep实例,这个dep 在访问器属性的 getter/setter 中被闭包引用,这个dep的作用是当目标对象属性发生写操作时,通知“感兴趣”的观察者;

2.如果属性是对象或者数组,则调用observe函数并把这个属性当做实参,目的是使目标对象深度可侦测;

3.使用Object.defineProperty函数把目标对象属性转成访问器属性,在getter方法里,通过执行dep.depend方法,收集对当前属性“感兴趣”的观察者;在setter方法里,执行observe(newVal),把新增加的属性值变成可侦测的,并执行dep.notify(),通知对此属性“感兴趣”的所有观察者。

export function defineReactive (
  obj: Object,
  key: string,
  val: any
) {
  const dep = new Dep()
  const property = Object.getOwnPropertyDescriptor(obj, key)
  // cater for pre-defined getter/setters
  const getter = property && property.get
  let val;
  if (!getter) {
    val = obj[key]
  }
  const setter = property && property.set
  let childOb = 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()
          if (Array.isArray(value)) {
            dependArray(value)
          }
        }
      }
      return value
    },
    set: function reactiveSetter (newVal) {
      const value = getter ? getter.call(obj) : val
      if (newVal === value) {
        return
      }
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      observe(newVal)
      dep.notify()
    }
  })
}
function dependArray (value: Array<any>) {
  for (let e, i = 0, l = value.length; i < l; i++) {
    e = value[i]
    e && e.__ob__ && e.__ob__.dep.depend()
    if (Array.isArray(e)) {
      dependArray(e)
    }
  }
}


2.3 Dep依赖收集器

顾名思义,Dep依赖收集器的作用就是收集观察者的

我们来看下Dep的实现,

1.Dep有个静态属性target,当观察者初始化时,会在观察者的构造方法里,执行观察者的get方法,在观察者的get方法里,观察者会把自己赋值给Dep.target,意味着当前的观察者是自己;

2.dep.addSub方法把当前的观察者收集,存储到subs属性中;

3.dep.notify方法会调用所有观察者的update方法。

let uid = 0
export default class Dep {
  static target: ?Watcher;
  id: number;
  subs: Array<Watcher>;
  constructor () {
    this.id = uid++
    this.subs = []
  }
  addSub (sub: Watcher) {
    this.subs.push(sub)
  }
  removeSub (sub: Watcher) {
    remove(this.subs, sub)
  }
  depend () {
    if (Dep.target) {
      Dep.target.addDep(this)
    }
  }
  notify () {
    const subs = this.subs.slice()
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
}
Dep.target = null
const targetStack = []
export function pushTarget (_target: ?Watcher) {
  if (Dep.target) targetStack.push(Dep.target)
  Dep.target = _target
}
export function popTarget () {
  Dep.target = targetStack.pop()
}


2.4 Watcher观察者

观察者的作用是监听目标对象的变化。观察者构造方法中的参数expOrFn,可以是表达式,如果是表达式的话,只接受键路径,例如"a.b.c";对于更复杂的表达式,可以使用一个函数替代。

我们来看下Watcher的实现,

1.Watcher的构造方法里执行get方法里,get方法里执行expOrFn,expOrFn中对目标对象进行求值,触发Dep收集观察者;

2.当目标对象更新时,会调用观察者的update方法,如果是同步更新则接着调用run方法,如果是异步更新则执行queueWatcher方法,但无论是同步更新还是异步更新,最终都会执行run方法;

3.在run方法里,执行get方法,重新求expOrFn的值,如果有cb参数,则调用cb函数,把新值和旧值当做实参传入。

let uid = 0
export default class Watcher {
  vm: Component;
  expression: string;
  cb: Function;
  id: number;
  getter: Function;
  value: any;
  constructor (
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: ?Object,
    isRenderWatcher?: boolean
  ) {
    this.vm = vm
    if (isRenderWatcher) {
      vm._watcher = this
    }
    vm._watchers.push(this)
    if (options) {
      this.deep = !!options.deep
    } else {
      this.deep = false
    }
    this.cb = cb
    this.id = ++uid
    this.expression = process.env.NODE_ENV !== 'production'
      ? expOrFn.toString()
      : ''
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn
    } else {
      this.getter = parsePath(expOrFn)
    }
    this.value = this.lazy
      ? undefined
      : this.get()
  }
  get () {
    pushTarget(this)
    let value
    const vm = this.vm
    try {
      value = this.getter.call(vm, vm)
    } catch (e) {
      throw e
    } finally {
      if (this.deep) {
        traverse(value)
      }
      popTarget()
    }
    return value
  }
  update () {
    if (this.lazy) {
      this.dirty = true
    } else if (this.sync) {
      this.run()
    } else {
      queueWatcher(this)
    }
  }
  run () {
    const value = this.get()
    if (
      value !== this.value ||
      isObject(value) ||
      this.deep
    ) {
      const oldValue = this.value
      this.value = value
      if (this.user) {
        try {
          this.cb.call(this.vm, value, oldValue)
        } catch (e) {
          handleError(e, this.vm, `callback for watcher "${this.expression}"`)
        }
      } else {
        this.cb.call(this.vm, value, oldValue)
      }
    }
  }
}


2.5 对对象属性进行增删操作的拦截

Object.defineProperty并不能拦截对象增删属性,Vue是通过Vue.set和Vue.delete实现对象增删属性拦截的。set方法里,首先将新加的属性设置为访问器属性,使其变为响应式,然后调用target.__ob__.dep.notify方法,通知观察者。del方法里,首先将属性从对象里删除,然后调用target.__ob__.dep.notify方法,通知观察者。

export function set (target: Object, key: any, val: any): any {
  if (key in target && !(key in Object.prototype)) {
    target[key] = val
    return val
  }
  const ob = (target: any).__ob__
  if (!ob) {
    target[key] = val
    return val
  }
  defineReactive(ob.value, key, val)
  ob.dep.notify()
  return val
}
export function del (target: Object, key: any) {
  const ob = (target: any).__ob__
  if (!hasOwn(target, key)) {
    return
  }
  delete target[key]
  if (!ob) {
    return
  }
  ob.dep.notify()
}


2.6 对数组操作的拦截

在defineReactive函数里,如果目标对象属性为数组,则对数组调用observe方法进行侦测。

let childOb = observe(val)


对数组的侦测,首先重写数组的原型为arrayMethods;然后遍历数组,对每一个元素调用observe函数。何为arrayMethods?首先设置arrayMethods的原型为Array.prototype;然后往arrayMethods上定义7个属性,这7个属性其实是重写的7个数组变异方法。有的数组变异方法是可以新增元素的,要把新增加的元素变成响应式的;在所有的变异方法里都会调用数组的__ob__.dep.notify方法通知观察者。示意图如下:

image.png

export class Observer {
  value: any;
  dep: Dep;
  constructor (value: any) {
    this.value = value
    this.dep = new Dep()
    def(value, '__ob__', this)
    if (Array.isArray(value)) {
      value.__proto__ = arrayMethods
      this.observeArray(value)
    } else {
      // ...
    }
  }
  observeArray (items: Array<any>) {
    for (let i = 0, l = items.length; i < l; i++) {
      observe(items[i])
    }
  }
}
const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)
const methodsToPatch = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]
methodsToPatch.forEach(function (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
  })
})


然后把目标对象属性设置为访问器属性,在访问器属性的get方法里,则执行childOb.dep.depend(),收集对此数组“感兴趣”的观察者;并调用dependArray,每个数组元素同样把对此数组“感兴趣”的观察者收集为依赖,这样保证每个数组元素变更时,会通知到对此数组“感兴趣”的观察者。

if (childOb) {
  childOb.dep.depend()
  if (Array.isArray(value)) {
    dependArray(value)
  }
}
function dependArray (value: Array<any>) {
  for (let e, i = 0, l = value.length; i < l; i++) {
    e = value[i]
    e && e.__ob__ && e.__ob__.dep.depend()
    if (Array.isArray(e)) {
      dependArray(e)
    }
  }
}


2.7 Vue2数据变更拦截的缺陷

2.7.1 Vue2可以拦截的数据变更:

对象属性的写操作;

非根级响应式对象的增删属性操作;

数组7个变异方法的拦截。


2.7.2 Vue2不能拦截的数据变更:

Vue 不允许动态添加根级响应式属性,所以你必须在初始化实例前声明所有根级响应式属性;

使用array[index] = item方式给数组元素赋值;

使用array.length = newLength方式改变数组长度。


三 Vue3响应式系统的实现

我们先整体看下Vue3响应式系统是怎么运作的,有个大体的概念,然后再拆分每一部分,看下每部分的实现。

3.1 先整体看下Vue3响应式系统的实现

目标对象经过reactive函数,生成Proxy代理对象,可以对5种操作进行拦截。这个过程就是“数据劫持”。示意图:

image.png


Vue3的观察者不叫Watcher,而是叫effect,它是基于ReactiveEffect接口实现的。effect初始化时,执行它的入参fn,fn里执行proxy对象的值,触发get/has/ownKeys trap。在get/has/ownKeys trap 里执行track方法,将目标对象属性和观察者存储到依赖收集表。这个过程就是“收集观察者”。示意图:

image.png


当proxy对象的值发生改变,触发deleteProperty/set trap。在deleteProperty/set trap 里执行trigger方法,从依赖收集表中找出目标对象属性对应的观察者set集合,遍历所有的观察者,执行run方法,最终会执行effect的入参fn函数。这个过程就是“通知观察者”。示意图:

image.png


Vue3响应式系统整体工作过程(鉴于掘金不支持视频,而gif最大支持5M,所以我把视频传到了B站):


3.2 reactive函数

reactive函数的作用就是生成目标对象的proxy代理对象。mutableHandlers包含proxy 拦截方法。rawToReactive、reactiveToRaw存储目标对象和proxy对象的映射关系。

export function reactive(target: object) {
  return createReactiveObject(
    target,
    rawToReactive,
    reactiveToRaw,
    mutableHandlers,
    mutableCollectionHandlers
  )
}
function createReactiveObject(
  target: unknown,
  toProxy: WeakMap<any, any>,
  toRaw: WeakMap<any, any>,
  baseHandlers: ProxyHandler<any>,
  collectionHandlers: ProxyHandler<any>
) {
  const handlers = baseHandlers
  observed = new Proxy(target, handlers)
  toProxy.set(target, observed)
  toRaw.set(observed, target)
  return observed
}


3.3 mutableHandlers

Vue3使用Proxy拦截了5个方法,在get/has/ownKeys trap 里通过track方法收集依赖,在deleteProperty/set trap 里通过trigger方法触发通知。

createGetter函数中,只有在用到某个对象时,才执行reactive函数对其进行数据劫持,生成proxy对象。

export const mutableHandlers: ProxyHandler<object> = {
  get: createGetter(false),
  set,
  deleteProperty,
  has,
  ownKeys
}
function createGetter(isReadonly: boolean, shallow = false) {
  return function get(target: object, key: string | symbol, receiver: object) {
    let res = Reflect.get(target, key, receiver)
    track(target, TrackOpTypes.GET, key)
    return isObject(res)
      ? reactive(res)
      : res
  }
}
function has(target: object, key: string | symbol): boolean {
  const result = Reflect.has(target, key)
  track(target, TrackOpTypes.HAS, key)
  return result
}
function ownKeys(target: object): (string | number | symbol)[] {
  track(target, TrackOpTypes.ITERATE, ITERATE_KEY)
  return Reflect.ownKeys(target)
}
function set(
  target: object,
  key: string | symbol,
  value: unknown,
  receiver: object
): boolean {
  const oldValue = (target as any)[key]
  const hadKey = hasOwn(target, key)
  const result = Reflect.set(target, key, value, receiver)
  if (target === toRaw(receiver)) {
    if (!hadKey) {
      trigger(target, TriggerOpTypes.ADD, key)
    } else if (hasChanged(value, oldValue)) {
      trigger(target, TriggerOpTypes.SET, key)
    }
  }
  return result
}
function deleteProperty(target: object, key: string | symbol): boolean {
  const hadKey = hasOwn(target, key)
  const oldValue = (target as any)[key]
  const result = Reflect.deleteProperty(target, key)
  if (result && hadKey) {
        trigger(target, TriggerOpTypes.DELETE, key)
  }
  return result
}


3.4 effect

Vue3的观察者不叫Watcher,而是叫effect,它是基于ReactiveEffect接口实现的。effect初始化时,执行它的入参fn,fn里执行proxy对象的值,触发get/has/ownKeys trap。

export function effect<T = any>(
  fn: () => T,   // 需要监听的函数
  options: ReactiveEffectOptions = EMPTY_OBJ
): ReactiveEffect<T> {
  const effect = createReactiveEffect(fn, options)
  if (!options.lazy) {
    effect()  // 非懒计算,则立即执行effect函数,effect函数内部执行run方法
  }
  return effect
}
function createReactiveEffect<T = any>(
  fn: () => T,
  options: ReactiveEffectOptions
): ReactiveEffect<T> {
  const effect = function reactiveEffect(...args: unknown[]): unknown {
    return run(effect, fn, args)
  } as ReactiveEffect
  effect._isEffect = true
  effect.active = true
  effect.raw = fn
  effect.deps = []
  effect.options = options
  return effect
}
function run(effect: ReactiveEffect, fn: Function, args: unknown[]): unknown {
  if (!effectStack.includes(effect)) {
    cleanup(effect) // 把当前观察者,从依赖收集表中删除,并把当前观察者的deps字段设置为空数组
    try {
      effectStack.push(effect) // 进栈
      return fn(...args)
    } finally {
      effectStack.pop()        // 出栈
    }
  }
}


3.5 track方法和trigger方法

track方法的作用是收集观察者到依赖收集表;trigger方法的作用是从依赖收集表中找到effect,并执行effect,最终会执行effect的实参,也就是fn函数。

export function track(target: object, type: TrackOpTypes, key: unknown) {
  const effect = effectStack[effectStack.length - 1]
  let depsMap = targetMap.get(target)
  let dep = depsMap.get(key)
  if (!dep.has(effect)) {
    dep.add(effect)             // 将观察者添加到依赖收集表的合适位置
    effect.deps.push(dep)       // 将依赖收集表的Dep添加到观察者的deps数组中
  }
}
export function trigger(
  target: object,
  type: TriggerOpTypes,
  key?: unknown,
  extraInfo?: DebuggerEventExtraInfo
) {
  const depsMap = targetMap.get(target)
  const effects = depsMap.get(key)
  const run = (effect: ReactiveEffect) => {
    effect()
  }
  effects.forEach(run)
}


四 Vue3和Vue2在响应式系统方面的对比

4.1 Vue3和vue2的响应式系统都采用观察者模式:

Vue3的响应式系统和Vue2一样,也是观察者模式(发布订阅者模式)。所以,Vue3的响应式系统同样包含三个阶段,1.数据劫持(变动侦测);2.收集依赖(观察者);3.通知依赖(观察者)。


4.2 Vue3相比Vue2在响应式系统方面的提升:

4.2.1 数据劫持的方式

Vue3的数据劫持是通过Proxy实现的,而Vue2是通过Object.defineProperty实现的;长远来看JS引擎会继续优化Proxy,但Object.defineProperty不会再有针对性的优化,所以Proxy性能上整体优于Object.defineProperty;

总结:Vue3比Vue2有更快的性能


4.2.2 支持数据劫持的数据类型

Vue3支持Object、Array、Map、WeakMap、Set、WeakSet六种数据类型的数据劫持,而Vue2只支持Object、Array两种数据类型;并且Vue3可以劫持对象的属性增删和数组的索引操作。

总结:Vue3支持更多数据类型的数据劫持


4.2.3 依赖收集的时机和触发通知的时机

Vue3在目标对象进行get/has/iterate三种操作时,进行依赖收集;而Vue2只在目标对象的属性进行get操作时,进行依赖收集;

Vue3在目标对象进行set/add/delete/clear四种操作时,触发通知依赖;而Vue2只在对目标对象的属性进行set操作时,触发通知依赖。

总结:Vue3支持更多的时机来进行依赖收集和触发通知


4.2.4 目标对象嵌套对象的数据劫持时机

Vue2会把整个data进行递归数据劫持,而Vue3只有在用到某个对象时,才对其进行数据劫持,所以Vue3响应式系统更快并且占用内存更小。想象下,一个很庞大的对象,我们并不是需要对其所有属性进行变动侦测,Vue2的方式就会导致无用的内存消耗和性能消耗。

image.png

总结:数据劫持方面,Vue3做到了“精准数据”的数据劫持,Vue3比Vue2占用更小的内存


4.2.5 依赖收集器的差异

Vue3通过一个WeakMap作为全局的依赖收集器,Vue3依赖收集器的结构是:

image.png

Vue2则是通过被闭包引用的dep和通过observer实例引用的dep来作为依赖收集器;

总结:Vue3的依赖收集器更容易维护,可以方便的找到或者移除目标对象的依赖。


五 总结

Vue3响应式系统显著优点是:有更快的性能占用更小的内存支持Vue根数据增删属性的拦截支持数组的拦截


需要技术交流可以加微信。