1. 程式人生 > >淺析Vue響應式原理(二)

淺析Vue響應式原理(二)

Vue響應式原理之Observer

之前簡單介紹了Dep和Watcher類的程式碼和作用,現在來介紹一下Observer類和set/get。在Vue例項後再新增響應式資料時需要藉助Vue.set/vm.$set方法,這兩個方法內部實際上呼叫了set方法。而Observer所做的就是將修改反映到檢視中。

Observer

export class Observer {
  value: any;
  dep: Dep;
  vmCount: number; // number of vms that has 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)) {
      const augment = hasProto
        ? protoAugment
        : copyAugment
      augment(value, arrayMethods, arrayKeys)
      this.observeArray(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])
    }
  }

  observeArray (items: Array<any>) {
    for (let i = 0, l = items.length; i < l; i++) {
      observe(items[i])
    }
  }
}

Observer有三個屬性。value是響應式資料的值;dep是Dep例項,這個Dep例項用於Vue.set/vm.$set中通知依賴更新;vmCount表示把這個資料當成根data物件的例項數量,大於0時是例項化傳入的根data物件。

建構函式接受一個值,表示要觀察的值,這樣,在Observer例項中引用了響應式資料,並將響應式資料的__ob__屬性指向自身。如果被觀察值是除陣列以外的型別,會呼叫walk方法,令每個屬性都是響應式。對於基本型別的值,Object.keys會返回一個空陣列,所以在walk內,defineReactive只在物件的屬性上執行。如果是被觀察值是陣列,那麼會在每個元素上呼叫工廠函式observe

,使其響應式。

對於陣列,響應式的實現稍有不同。回顧一下在教程陣列更新檢測裡的說明,變異方法會觸發檢視更新。其具體實現就在這裡。arrayMethods是一個物件,儲存了Vue重寫的陣列方法,具體重寫方式下面再說,現在只需知道這些重寫的陣列方法除了保持原陣列方法的功能外,還能通知依賴資料已更新。augment的用途是令value能夠呼叫在arrayMethods中的方法,實現的方式有兩種。第一種是通過原型鏈實現,在value.__proto__新增這些方法,優先選擇這種實現。部分瀏覽器不支援__proto__,則直接在value上新增這些方法。

最後執行observeArray方法,遍歷value

,在每個元素上執行observe方法。

陣列變異方法的實現

執行變異方法會觸發檢視功能,所以變異方法要實現的功能,除了包括原來陣列方法的功能外,還要有通知依賴資料更新的功能。程式碼儲存在/src/core/observer/array.js

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)
    // notify change
    ob.dep.notify()
    return result
  })
})

模組內,使用arrayProto儲存陣列原型,arrayMethods的原型是arrayProto,用來儲存變異後的方法,methodsToPatch是儲存變異方法名的陣列。

遍歷methodsToPatch,根據方法名來獲取在arrayProto上的陣列變異方法,然後在arrayMethods實現同名方法。

在該同名方法內,首先執行快取的陣列方法original,執行上下文是this,這些方法最終會新增到響應式陣列或其原型上,所以被呼叫時this是陣列本身。ob指向this.__ob__,使用inserted指向被插入的元素,呼叫ob.observeArray觀察新增的陣列元素。最後執行ob.dep.notify(),通知依賴更新。

observe

工廠函式,獲取value上__ob__屬性指向的Observer例項,如果需要該屬性且未定義時,根據資料建立一個Observer例項,在例項化時會在value上新增__ob__屬性。引數二表示傳入的value是否是根data物件。只有根資料物件的__ob__.vmCount大於0。

isObject判斷value是不是Object型別,實現如obj !== null && typeof obj === 'object'

export function observe (value: any, asRootData: ?boolean): Observer | void {
  if (!isObject(value) || value instanceof VNode) {
    return
  }
  let ob: Observer | void
  if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
    ob = value.__ob__
  } else if (
    shouldObserve &&
    !isServerRendering() &&
    (Array.isArray(value) || isPlainObject(value)) &&
    Object.isExtensible(value) &&
    !value._isVue
  ) {
    ob = new Observer(value)
  }
  if (asRootData && ob) {
    ob.vmCount++
  }
  return ob
}

此處可以看出,value與Observer例項ob之間是雙向引用。value.__ob__指向ob,ob.value指向value

Vue.set

在Vue例項化以後,如果想為其新增新的響應式屬性,對於物件,直接使用字面量賦值是沒有效果的。由響應式資料的實現可以想到,這種直接賦值的方式,並沒有為該屬性自定義getter/setter,在獲取屬性時不會收集依賴,在更新屬性時不會觸發更新。如果想要為已存在的響應式資料新增新屬性,可以使用Vue.set/vm.$set方法,但要注意,不能在data上新增新屬性。

Vue.set/vm.$set內部都是在/src/code/observer/index.js定義的set的函式。

set函式接受三個引數,引數一target表示要新增屬性的物件,引數二key表示新增的屬性名或索引,引數三val表示新增屬性的初始值。

export function set (target: Array<any> | Object, key: any, val: any): any {
  if (process.env.NODE_ENV !== 'production' &&
    !Array.isArray(target) &&
    !isObject(target)
  ) {
    warn(`Cannot set reactive property on non-object/array value: ${target}`)
  }
  if (Array.isArray(target) && isValidArrayIndex(key)) {
    target.length = Math.max(target.length, key)
    target.splice(key, 1, val)
    return val
  }

  if (key in target && !(key in Object.prototype)) {
    target[key] = val
    return val
  }
  const ob = (target: any).__ob__
  if (target._isVue || (ob && ob.vmCount)) {
    process.env.NODE_ENV !== 'production' && warn(
      'Avoid adding reactive properties to a Vue instance or its root $data ' +
      'at runtime - declare it upfront in the data option.'
    )
    return val
  }
  // 不存在ob 說明不是響應式資料
  if (!ob) {
    target[key] = val
    return val
  }
  // 為target新增新屬性
  defineReactive(ob.value, key, val)
  // ob.dep實際是target.__ob__.dep
  ob.dep.notify()
  return val
}

函式內部首先判斷target型別,非陣列或非物件的目標資料是無法新增響應式資料的。

如果是陣列,且key是有效的陣列索引,更新陣列長度,然後呼叫變異方法splice,更新對應的值並觸發檢視更新。如果是物件,且屬性keytarget的原型鏈上且不在Object.prototype上(即不是Object原型上定義的屬性或方法),直接在target上新增或更新key

ob指向target.__ob__,如果target是Vue例項或是根data物件(ob.vmCount > 0),則無法新增資料,直接返回。

接著處理能為target新增屬性的情況。不存在ob時,說明不是響應式資料,直接更新target。否則,執行defineReactive函式為ob.value新增響應式屬性,ob.value實際指向target,新增之後呼叫ob.dep.notify()通知觀察者重新求值,ob是Observer例項。

總結一下,set的內部邏輯:

target是陣列時,更新長度,呼叫變異方法splice插入新元素即可。

target是物件時:

  • key在除Object.prototype外的原型鏈上時,直接賦值
  • key在原型鏈上搜索不到時,需要新增屬性。如果target__ob__屬性,說明不是響應式資料,直接賦值。否則呼叫defineReactive(ob.value, key, val)觀察新資料,同時觸發依賴。

Vue.delete

刪除物件的屬性。如果物件是響應式的,確保刪除能觸發更新檢視。

Vue.delete實際指向deldel接受兩個引數,引數一target表示要刪除屬性的物件,引數二key表示要刪除的屬性名。

如果target是陣列且key對於的索引在target中存在,使用變異方法splice方法直接刪除。

如果target是Vue例項或是根data物件則返回,不允許在其上刪除屬性。key不是例項自身屬性時也返回,不允許刪除。如果是自身屬性則使用delete刪除,接著判斷是否有__ob__屬性,如果有,說明是響應式資料,執行__ob__.dep.notify通知檢視更新。

export function del (target: Array<any> | Object, key: any) {
  if (process.env.NODE_ENV !== 'production' &&
    !Array.isArray(target) &&
    !isObject(target)
  ) {
    warn(`Cannot delete reactive property on non-object/array value: ${target}`)
  }
  // 陣列 直接刪除元素
  if (Array.isArray(target) && isValidArrayIndex(key)) {
    target.splice(key, 1)
    return
  }
  const ob = (target: any).__ob__
  if (target._isVue || (ob && ob.vmCount)) {
    process.env.NODE_ENV !== 'production' && warn(
      'Avoid deleting properties on a Vue instance or its root $data ' +
      '- just set it to null.'
    )
    return
  }
  // 屬性不在target上
  if (!hasOwn(target, key)) {
    return
  }
  delete target[key]
  // 不是響應式資料
  if (!ob) {
    return
  }
  ob.dep.notify()
}

小結

關於Observer類和set/get的原始碼已經做了簡單的分析,細心的讀者可能會有一個問題:target.__ob__.dep是什麼時候收集依賴的。答案就在defineReactive的原始碼中,其收集操作同樣在響應式資料的getter中執行。

至於defineReactive的原始碼解析,在後面的文章再做分析。

參考連結