Vue響應式系統技術原理和Vue3響應式系統的優勢

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


一 前言

數據驅動視圖是MVVM框架的顯著特色,MVVM框架的出現將前端開發者從繁雜的、「鳥巢」般的dom操做中解放出來,開發體驗比jQuery/underscore模板提高了不知道幾個層次。前端

想要實現數據驅動視圖,須要解決兩個問題,一是框架要知道數據何時變動、二是框架如何把變動後的數據更新到視圖。對於解決第一個問題,React是經過開發者手動執行this.setState方法實現的,Vue框架是經過自身數據響應式系統實現的;對於解決第二個問題,React和Vue都是經過patch函數和virtual dom diff算法實現的。vue

這篇文章裏,咱們只關注Vue的響應式系統是如何實現的。不管是Vue2仍是Vue3,二者的響應式系統都是基於觀察者模式(發佈訂閱模式)實現的,因此都涉及這麼幾個概念:目標對象(target)、依賴收集器(Dep)、觀察者(Watcher)。依賴收集器收集目標對象的觀察者,當目標對象的狀態發生改變,全部的觀察者都將獲得通知。示意圖以下:java

image.png


二 Vue2響應式系統的實現

咱們先總體看下Vue2響應式系統是怎麼運做的,有個大致的概念,而後再拆分每一部分,看下每部分的實現。react

2.1 先總體看下Vue2響應式系統的實現

目標對象通過observe函數,新增__ob__屬性,這個屬性是一個Observer實例,這個Observer實例含有dep屬性,dep屬性指向依賴收集者。而後,對目標對象的每個屬性執行defineReactive函數,將屬性轉換成訪問器屬性,這樣咱們就能夠對屬性的讀寫操做進行攔截。這個過程稱之爲「數據劫持」。算法

Kapture 2019-12-12 at 11.38.26.gif


當執行觀察者get方法時,會觸發目標對象屬性的getter方法,在getter方法裏收集觀察者,這個過程就是「收集觀察者」。express

當目標對象屬性變動時,會觸發目標對象的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根數據增刪屬性的攔截支持數組的攔截


須要技術交流能夠加微信。

相關文章
相關標籤/搜索