vue中是如何監聽數組變化?

vue中是如何監聽數組變化?

我們知道通過Object.defineProperty()劫持數組爲其設置getter和setter後,調用的數組的push、splice、pop等方法改變數組元素時並不會觸發數組的setter,這就會造成使用上述方法改變數組後,頁面上並不能及時體現這些變化,也就是數組數據變化不是響應式的(對上述不瞭解的可以參考這篇文章)。但實際用vue開發時,對於響應式數組,使用push、splice、pop等方法改變數組時,頁面會及時體現這種變化,那麼vue中是如何實現的呢?

通過vue源碼可以看出,vue重寫了數組的push、splice、pop等方法。點擊查看源碼

// src/core/observer/array.js

// 獲取數組的原型Array.prototype,上面有我們常用的數組方法
const arrayProto = Array.prototype
// 創建一個空對象arrayMethods,並將arrayMethods的原型指向Array.prototype
export const arrayMethods = Object.create(arrayProto)

// 列出需要重寫的數組方法名
const methodsToPatch = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]
// 遍歷上述數組方法名,依次將上述重寫後的數組方法添加到arrayMethods對象上
methodsToPatch.forEach(function (method) {
  // 保存一份當前的方法名對應的數組原始方法
  const original = arrayProto[method]
  // 將重寫後的方法定義到arrayMethods對象上,function mutator() {}就是重寫後的方法
  def(arrayMethods, method, function mutator (...args) {
    // 調用數組原始方法,並傳入參數args,並將執行結果賦給result
    const result = original.apply(this, args)
    // 當數組調用重寫後的方法時,this指向該數組,當該數組爲響應式時,就可以獲取到其__ob__屬性
    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()
    // 最後返回執行結果result
    return result
  })
})

從上面可以看出array.js中重寫了數組的push、pop、shift、unshift、splice、sort、reverse七種方法,重寫方法在實現時除了將數組方法名對應的原始方法調用一遍並將執行結果返回外,還通過執行ob.dep.notify()將當前數組的變更通知給其訂閱者,這樣當使用重寫後方法改變數組後,數組訂閱者會將這邊變化更新到頁面中。

重寫完數組的上述7種方法外,我們還需要將這些重寫的方法應用到數組上,因此在Observer構造函數中,可以看到在監聽數據時會判斷數據類型是否爲數組。當爲數組時,如果瀏覽器支持__proto__,則直接將當前數據的原型__proto__指向重寫後的數組方法對象arrayMethods,如果瀏覽器不支持__proto__,則直接將arrayMethods上重寫的方法直接定義到當前數據對象上;當數據類型爲非數組時,繼續遞歸執行數據的監聽。

// src/core/observer/index.js
export class Observer {
  ...
  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)
    }
  }
  ...
}
function protoAugment (target, src: Object) {
  /* eslint-disable no-proto */
  target.__proto__ = src
  /* eslint-enable no-proto */
}
function copyAugment (target: Object, src: Object, keys: Array<string>) {
  for (let i = 0, l = keys.length; i < l; i++) {
    const key = keys[i]
    def(target, key, src[key])
  }
}

經過上述處理後,對於數組,當我們調用其方法處理數組時會按照如下原型鏈來獲取數組方法:

在這裏插入圖片描述

對於響應式數組,當瀏覽器支持__proto__屬性時,使用push等方法時先從其原型arrayMethods上尋找push方法,也就是重寫後的方法,處理之後數組的變化會通知到其訂閱者,更新頁面,當在arrayMethods上查詢不到時會向上在Array.prototype上查詢;當瀏覽器不支持__proto__屬性時,使用push等方法時會先從數組自身上查詢,如果查詢不到會向上再Array.prototype上查詢。

對於非響應式數組,當使用push等方法時會直接從Array.prototype上查詢。

值得一提的是源碼中通過判斷瀏覽器是否支持__proto__來分別使用protoAugment和copyAugment 方法將重寫後的數組方法應用到數組中,這是因爲對於IE10及以下的IE瀏覽器是不支持__proto__屬性的:
在這裏插入圖片描述
上述截圖參考於Vue源碼解析五——數據響應系統

參考文獻:

[1] 如何監聽數組變化?

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章