來(lái)不及解釋了,快上車(chē)......
之前的一篇文章 vue2.x響應(yīng)式原理主要是對(duì)象的響應(yīng)式盏求,今天補(bǔ)充一下數(shù)組響應(yīng)式的原理筛峭,因?yàn)関ue對(duì)數(shù)組做了特別的處理箭启。
vue 為什么沒(méi)像處理對(duì)象一樣用 Object.defineProperty 處理數(shù)組蜜氨?是 Object.defineProperty 無(wú)法監(jiān)測(cè)數(shù)組嗎炎滞?又或者是出于其它方面的什么考慮呢蚜迅?那它是怎么實(shí)現(xiàn)對(duì)數(shù)組的監(jiān)聽(tīng)的舵匾?帶著這些問(wèn)題我們?nèi)ヒ惶骄烤?..
Object.defineProperty 支持?jǐn)?shù)組嗎
首先我們來(lái)做一個(gè)測(cè)試,看 Object.defineProperty 是否支持?jǐn)?shù)組谁不。
function defineReactive(obj, key, val) {
Object.defineProperty(obj, key, {
enumerable: true, // 屬性可枚舉
configurable: true,
get() {
console.log('??-----讀取', val)
return val;
},
set(newVal) {
if (val === newVal) return
val = newVal
console.log('??-----改變---', val, obj)
}
})
}
還是這個(gè) defineReactive 函數(shù)坐梯,我們通過(guò)它來(lái)遍歷數(shù)組,用數(shù)組的索引作為 key刹帕,來(lái)給每一項(xiàng)打上getter/setter吵血。
let array = [1,2,3,4,5]
array.forEach((c,index) => {
defineReactive(array, index, c)
})
我們?cè)诳刂婆_(tái)打印一下,可以看到打印的結(jié)果偷溺,這說(shuō)明數(shù)組項(xiàng)被打上了 getter/setter蹋辅,還回到這個(gè)問(wèn)題,Object.defineProperty 可以做到對(duì)數(shù)組的監(jiān)聽(tīng)挫掏,它是支持?jǐn)?shù)組的侦另。
vue為什么沒(méi)有提供對(duì)數(shù)組屬性的監(jiān)聽(tīng)呢
第二個(gè)為題就來(lái)了,既然 Object.defineProperty 有這個(gè)能力砍濒,那么 vue 為什么沒(méi)用它來(lái)實(shí)現(xiàn)對(duì)數(shù)組屬性的監(jiān)聽(tīng)呢
有人提到 length 屬性淋肾,length 屬性的改變可能會(huì)導(dǎo)致一些空元素,的確爸邢,Object.defineProperty 不能處理這些為空的數(shù)組樊卓。但是我們換個(gè)角度想一下,通過(guò)改變 length 屬性去增加數(shù)組長(zhǎng)度杠河,不就是相當(dāng)于增加屬性嗎碌尔,這個(gè)在對(duì)象里面 vue 也是無(wú)法監(jiān)測(cè)到的浇辜。
在上圖第二個(gè) log 里面,當(dāng)給數(shù)組某一項(xiàng)賦值的時(shí)候唾戚,觸發(fā)了 setter柳洋,setter 的時(shí)候,打印 obj 數(shù)組又依次讀取了數(shù)組的值叹坦,這會(huì)影響性能熊镣。另外很多時(shí)候數(shù)組長(zhǎng)度我們并不確定,無(wú)法提前打上 getter/setter募书,而且如果數(shù)組長(zhǎng)度很大也會(huì)造成性能問(wèn)題绪囱,用尤大的原話說(shuō)就是性能代價(jià)和獲得的用戶(hù)體驗(yàn)收益不成正比。
貼一個(gè)尤大在github上的回答莹捡。
另外賀師俊老濕的回答也非常精辟??
:如果你知道數(shù)組的長(zhǎng)度鬼吵,理論上是可以預(yù)先給所有的索引設(shè)置 getter/setter 的。但是一來(lái)很多場(chǎng)景下你不知道數(shù)組的長(zhǎng)度篮赢,二來(lái)齿椅,如果是很大的數(shù)組,預(yù)先加 getter/setter 性能負(fù)擔(dān)較大启泣。
總而言之就是理論上 vue 是可以這樣做涣脚,但是出于性能考慮沒(méi)這樣做,而是用了一種數(shù)組變異辦法來(lái)觸發(fā)視圖更新种远。
vue如何實(shí)現(xiàn)對(duì)數(shù)組的監(jiān)聽(tīng)
解決了上面的兩個(gè)問(wèn)題涩澡,我們接下來(lái)就來(lái)看看 vue 的數(shù)組變異具體是怎么實(shí)現(xiàn)的。
const arrayKeys = Object.getOwnPropertyNames(arrayMethods)
if (Array.isArray(value)) {
const augment = hasProto
? protoAugment
: copyAugment
augment(value, arrayMethods, arrayKeys)
this.observeArray(value)
} else {
this.walk(value)
}
observeArray 會(huì)把數(shù)組里面的對(duì)象數(shù)據(jù)變成是可偵測(cè)的響應(yīng)式數(shù)據(jù)坠敷,observeArray 和 walk 函數(shù)主要還是我們上篇文章vue2.x響應(yīng)式原理的內(nèi)容。vue在這里對(duì)數(shù)組進(jìn)行了特別處理射富,就是依靠著 protoAugment膝迎、copyAugment 這兩個(gè)函數(shù)。
export const hasProto = '__proto__' in {}
首先通過(guò) hasProto 判斷瀏覽器是否支持 proto 屬性胰耗,來(lái)決定是執(zhí)行 protoAugment 還是 copyAugment限次。再看這兩個(gè)函數(shù)之前,我們先來(lái)看一下數(shù)組變異的核心文件 array.js,順便也能知道 arrayMethods, arrayKeys 這兩個(gè)參數(shù)是什么柴灯。
// array.js
import { def } from '../util/index'
const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)
/**
* Intercept mutating methods and emit events
*/
;[
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
.forEach(function (method) {
// cache original method
const original = arrayProto[method]
def(arrayMethods, method, function mutator (...args) {
// 調(diào)用數(shù)組真正的方法
const result = original.apply(this, args)
// __ob__代表數(shù)據(jù)是否被observe了
const ob = this.__ob__
let inserted
switch (method) {
case 'push':
case 'unshift':
inserted = args
break
case 'splice':
inserted = args.slice(2)
break
}
// inserted表示有數(shù)據(jù)插入 對(duì)新數(shù)據(jù)進(jìn)行observe
if (inserted) ob.observeArray(inserted)
// notify change
ob.dep.notify()
return result
})
})
/**
* Define a property.
*/
export function def (obj: Object, key: string, val:any,enumerable?:boolean) {
Object.defineProperty(obj, key, {
value: val,
enumerable: !!enumerable,
writable: true,
configurable: true
})
}
vue 通過(guò) Object.create讓arrayMethods 繼承了數(shù)組的原型卖漫,Object.create 本質(zhì)是原型式繼承【手撕JS繼承】,此時(shí)數(shù)組的原型掛到了arrayMethods 的原型鏈上赠群。
def函數(shù)主要來(lái)定義屬性羊始。遍歷這七種方法,通過(guò) def 函數(shù)查描,我們重寫(xiě)了 arrayMethods 原型鏈上的這七種方法突委,并在內(nèi)部調(diào)用了數(shù)組的原始方法柏卤,最后通過(guò) notify 更新視圖。inserted 代表新數(shù)據(jù)插入匀油,需要對(duì)新數(shù)據(jù)進(jìn)行 obsserve缘缚。
下面我們?cè)倩仡^來(lái)看 protoAugment 和 copyAugment 這兩個(gè)函數(shù)。
/**
* Augment an target Object or Array by intercepting
* the prototype chain using __proto__
*/
function protoAugment (target, src: Object, keys: any) {
/* eslint-disable no-proto */
target.__proto__ = src
/* eslint-enable no-proto */
}
/**
* Augment an target Object or Array by defining
* hidden properties.
*/
/* istanbul ignore next */
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])
}
}
protoAugment 讓數(shù)組的原型鏈指向了 arrayMethods敌蚜。
當(dāng)瀏覽器不支持 __proto__
時(shí)候桥滨,遍歷 arrayKeys,通過(guò) def 函數(shù)弛车,我們手動(dòng)把 arrayMethods 上的方法掛載到目標(biāo)數(shù)據(jù)上齐媒,相當(dāng)于是個(gè) polyfill。唯一的區(qū)別是 protoAugment 是把 arrayMethods 掛到了目標(biāo)的原型鏈上帅韧,而 copyAugment 則是直接把 arrayMethods 的方法定義到了目標(biāo)的屬性上里初。
小結(jié)
vue 出于性能的考慮,沒(méi)有用 Object.defineProperty 去監(jiān)聽(tīng)數(shù)組忽舟,而是通過(guò)覆蓋數(shù)組的原型的方法双妨,對(duì)常用的七種方法進(jìn)行了變異,以此來(lái)實(shí)現(xiàn)對(duì)數(shù)組的監(jiān)聽(tīng)叮阅。