博客 / 詳情

返回

Vue 響應式原理剖析 —— 數據更新常見問題

概況

在 Vue 開發的過程中,多少都會遇到數據更新後,頁面沒有更新渲染這類問題。而在上兩篇文章《Vue 響應式原理剖析 —— 從實例化、渲染到數據更新(上)》和《Vue 響應式原理剖析 —— 從實例化、渲染到數據更新(下)》中,從「實例化」、「渲染」、「數據更新」三條線完整地講述了 Vue「響應式」的工作原理,本文正是基於這些原理去解決一些常見的數據更新相關問題。

對象數據的某些修改無法被檢聽?

如下的一個場景,obj.message 賦值時能否被監聽響應呢?

var vm = new Vue({
    data: {
        obj: {
            a: 1
        },
    },
    template: '<div>{{ obj.message }}</div>'
});

vm.obj.message = 'modified';

答案是不能被監聽的。原因:對象屬性的添加和刪除無法被 Object.defineProperty 監聽,正如前文所述,Vue 的數據響應式基於 Object.defineProperty 實現,因此也受限。

解決辦法: Vue 提供了特定的方法 vm.$set(obj, propertyName, newValue) 來處理這種情況,至於該方法的具體邏輯,後面會詳細展開説明。

數組數據的某些修改無法被監聽?

如下的一個場景,三個賦值語句裏面,哪個能被監聽響應呢?

const vm = new Vue({
    data: {
        items: [1, 2, 3, 4, 5],
    },
});
vm.items[1] = 8;
vm.items[5] = 6;
vm.items.length = 2;

答案是三個操作都不能被監聽到。原因:

  • 第二個操作 vm.items[5] = 6 應該是比較明顯的,與上面對象添加和刪除屬性類似,數組新添加的元素和刪除元素無法被 Object.defineProperty 監聽。
  • 第三個操作 vm.items.length = 2 也是由於 Object.defineProperty 的限制,數組的長度直接修改也無法被監聽。
  • 最容易誤判的可能是第一個操作 vm.items[1] = 8,一種較為常規的説法是 Object.defineProperty 不支持監聽數組元素的變化,要驗證這個説法可以直接用一個例子説明真實的情況。

如下的一個例子:

function defineReactive(obj, key, val) {
    Object.defineProperty(obj, key, {
        enumerable: true,
        configurable: true,
        get() {
            console.log('讀取 index ' + key, '當前值是 ' + val)
            return val;
        },
        set(newVal) {
            if (val === newVal) {
                return;
            }
            val = newVal
            console.log('修改 index ' + key, '新值是 ' + val)
        }
    })
}

const testArray = [1, 2, 3, 4, 5]

testArray.forEach((c, index) => {
    defineReactive(testArray, index, c)
});

testArray[0] = 100;
testArray[5] = 600;

數組監聽示例結果

也可以點擊這裏打開示例查看控制枱輸出。可以看到,超出範圍的數組元素操作 Object.defineProperty 確實無法監聽,但範圍內的元素重新賦值是可以被監聽的。那麼為什麼在 Vue 中對數組類型的 data,直接使用下標賦值無法被監聽呢?

答案是出於性能考慮,從上面的基礎例子中可以看到,對象和數組如果需要監聽每個屬性和元素,實際上是對每個屬性或者元素進行 Object.defineProperty 劫持,對象是監聽 key 而數組則是以數字下標作為 key,數組的數據量可能會很大,因此 Vue 出於性能考慮,並沒有對元素下標進行響應式處理。作為補充,Vue 對數組原型鏈上的幾個方法進行劫持,對於會導致元素新增的3個方法 pushpopunshift 會在內部獲取新增的元素,執行響應式的處理:

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
  })
})

與對象類似,如果需要為數字元素重新賦值,可以使用 vm.$set(arr, indexOfItem, newValue) 方法,這裏展示一下 $set 的具體實現:

// 精簡了非 production 邏輯
export function set (target: Array<any> | Object, key: any, val: any): any {
  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
  }
  if (!ob) {
    target[key] = val
    return val
  }
  defineReactive(ob.value, key, val)
  ob.dep.notify()
  return val
}

主要的邏輯包括以下操作:

  1. 這個方法同時用於對象和數組,因此第一步會檢驗傳入的 target 是否為數組,並且傳入的 key(即數組的數字下標)是否符合數組的長度範圍(如上面所述,Object.defineProperty 不支持劫持新添加的元素),符合的元素會調用 splice 插入到數組中,由於 splice 已經被劫持,新增加的元素會進行「響應式」處理。
  2. 判斷如果 key 原先已存在,則無需再監聽。
  3. 判斷是根節點 Vue,即最外層的 Vue,或者已經有 __ob__ 屬性(表示已經進行響應式處理,詳情可以瀏覽前文),則無需再進行監聽。
  4. 如果不符合前面的條件,則表明該屬性需要執行「響應式」處理,會調用 defineReactive 方法(響應式數據封裝的入口方法,詳情可以瀏覽前文)。
user avatar sunshine_591c4563d4a83 頭像 smile1213 頭像 yunuo_5f87fbee283af 頭像 jyeontu 頭像 nidexiaoxiongruantangna 頭像 3yya 頭像 zhengcaiyunqianduantuandui 頭像 shijuepaipie 頭像 eraitianshi 頭像
9 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.