Vue數組的劫持逐步分析講解

一,前言

上篇,主要介紹瞭 Vue 數據初始化流程中,對象屬性的深層劫持是如何實現的

核心思路就是遞歸,主要流程如下;

1.通過 data = isFunction(data) ? data.call(vm) : data;處理後的 data 一定是對象類型

2.通過 data = observe(data)處理後的 data 就實現瞭數據的響應式(目前隻有劫持)

3.observe 方法最終返回一個 Observer 類

4.Observer 類初始化時,通過 walk 遍歷屬性

5.對每一個屬性進行 defineReactive(Object.defineProperty)就實現對象屬性的單層數據劫持

6.在 defineReactive 中,如果屬性值為對象類型就繼續調用 observe 對當前的對象屬性進行觀測(即遞歸步驟 3~5),這樣就實現瞭對象屬性的深層數據劫持

本篇,繼續介紹 Vue 數據初始化流程中,對於數組類型的劫持

二,對象劫持回顧

1,Demo

data 數據中對象屬性的深層觀測,即對象屬性為對象(包含多層)的情況

let vm = new Vue({
  el: '#app',
  data() {
    return { message: 'Hello Vue', obj: { key: "val" }, a: { a: { a: {} } } }
});

當 data 中的屬性為數組時,Vue 是如何進行處理的

三,數組類型的處理

1,當前邏輯分析

按照當前版本的處理邏輯,所有對象類型會對被進行深層觀測,數組也不例外

let vm = new Vue({
  el: '#app',
  data() {
    return { message: 'Hello Vue', obj: { key: "val" }, arr:[1,2,3]}
  }
});

可以看到,數組中的每一項,都被添加瞭 get、set 方法,也就相當於實現瞭對數組的深層觀測

備註:Object.defineProperty支持數組數據類型的劫持

2,Vue 對性能的權衡

在 Vue2.x 中,不支持通過修改數組索引和長度的數據劫持;

那麼,為什麼原本可以實現對數組索引的觀測,Vue 卻選擇瞭不支持呢?

主要是考慮瞭性能問題,比如,數組中的數據量非常大時:

let vm = new Vue({
  el: '#app',
  data() {
    return { arr:new Array(9999) }
  }
});

這時,數組中 9999 條數據,將全部被添加 get、set 方法

而這一套操作就比較費勁瞭:為瞭實現數組索引劫持,需要對數組中每一項進行處理

還有就是,雖然數組能夠通過 defineProperty 實現對索引更新劫持

但在實際開發場景真的需要嗎?似乎很少會使用 arr[888] = x 這種操作

所以,權衡性能和需求,Vue 源碼中沒有采用 defineProperty 對數組進行處理

當然,這也就導致瞭在 Vue 中無法通過直接修改索引、length 觸發視圖的更新

3,數組的劫持思路

核心目標是要實現數組的響應式:

Vue 認為這 7 個方法能夠改變原數組:push、pop、splice、shift、unshift、reverse、sort

所以,隻要對這 7 個方法進行處理,就能劫持到數組的數據變化,實現數組數據的響應式

備註:這種實現思路,也直接導致瞭 vue2 修改數組的索引和長度不能觸發視圖更新

梳理對象屬性深層劫持的實現:

  • 數據觀測入口:src/observe/index.js#observe方法
  • 如果數據為對象類型就 new Observer
  • Observer 初始化時,會遍歷對象屬性,逐一遞歸 Object.defineProperty

數組也是對象,所以,要把數組的處理邏輯單獨拆出來。即對 7 個變異方法進行重寫

// src/utils
/**
 * 判斷是否是數組
 * @param {*} val 
 * @returns 
 */
export function isArray(val) {
  return Array.isArray(val)
}
// src/observe/index.js
import { arrayMethods } from "./array";
class Observer {
  constructor(value) {
    if(isArray(value)){
      // 對數組類型進行單獨處理:重寫 7 個變異方法
    }else{
      this.walk(value);
    }
  }
}

4,數組方法的攔截思路

  • 重寫方法需要在原生方法基礎上,實現對數據變化的劫持操作
  • 僅對響應式數據中的數組進行方法重寫,不能影響非響應式數組

所以,對響應式數據中數組這 7 個方法進行攔截,即優先使用重寫方法,其他方法還走原生邏輯

數組方法的查找,先查找自己身上的方法(即重寫方法),找不到再去鏈上查(原生方法)

5,數組方法重寫的實現

// src/Observer/array.js
// 拿到數組的原型方法
let oldArrayPrototype = Array.prototype;
// 原型繼承,將原型鏈向後移動 arrayMethods.__proto__ == oldArrayPrototype
export let arrayMethods = Object.create(oldArrayPrototype);
// 重寫能夠導致原數組變化的七個方法
let methods = [
  'push',
  'pop',
  'shift',
  'unshift',
  'reverse',
  'sort',
  'splice'
]
// 在數組自身上進行方法重寫,對鏈上的同名方法進行攔截
methods.forEach(method => {
  arrayMethods[method] = function () {
    console.log('數組的方法進行重寫操作 method = ' + method)
  }
});

添加 new Observer 時,對數組方法重寫的邏輯:

// src/observe/index.js
import { arrayMethods } from "./array";
class Observer {
  constructor(value) {
    // 分別處理 value 為數組和對象兩種情況
    if(isArray(value)){
      value.__proto__ = arrayMethods; // 更改數組的原型方法
    }else{
      this.walk(value);
    }
  }
}

測試數組方法的重寫:

數組的鏈:

  • array.proto:包含 7 個重寫方法
  • array.proto.proto:原始方法

6,數組方法攔截的實現

// src/state.js#initData
function initData(vm) {
    let data = vm.$options.data;
    data = isFunction(data) ? data.call(vm) : data;
    observe(data);	// 在observe方法中new Observer執行後,數組的原型方法已完成重寫
    // 測試數組方法的攔截效果
    data.arr.push(666); 
    data.arr.pop()
}

  • arrayMethods.push:會在數組自身找到重寫的push方法,不會繼續到鏈上查找,實現攔截
  • arrayMethods.pop:數組自身沒找到重寫方法,繼續到鏈上找到原生pop方法

四,結尾

本篇主要介紹瞭 Vue 數據初始化流程中,數組類型的數據劫持,核心有以下幾點:

出於對性能的考慮,Vue 沒有對數組類型的數據使用 Object.defineProperty 進行遞歸劫持,而是通過對能夠導致原數組變化的 7 個方法進行攔截和重寫實現瞭數據劫持

下一篇,數據代理的實現

到此這篇關於Vue數組的劫持逐步分析講解的文章就介紹到這瞭,更多相關Vue數組劫持內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!

推薦閱讀: