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!