前言
這章節(jié)承上章節(jié)漏掉的數(shù)組觀測稳析、新增屬性觀測
正文
觀測數(shù)組
我們回到Observer
的這段代碼
if (Array.isArray(value)) {
const augment = hasProto
? protoAugment
: copyAugment
augment(value, arrayMethods, arrayKeys)
this.observeArray(value)
} else {
this.walk(value)
}
上章節(jié)我們走的是else
读慎,這次我們走if
崇众,即value
是數(shù)組的情況
我們先判斷當(dāng)前環(huán)境是否支持__proto__
岭参,看情況分別使用protoAugment到千、copyAugment
,將其賦值給augment
augment(value, arrayMethods, arrayKeys)
我們先搞清楚arrayMethods齐邦、arrayKeys
分別是什么
export const arrayMethods = Object.create(arrayProto)
const arrayKeys = Object.getOwnPropertyNames(arrayMethods)
溯源可知arrayMethods
就是數(shù)組的原型對象椎侠,所以我們再看protoAugment、copyAugment
function protoAugment(target, src: Object, keys: any) {
target.__proto__ = src
}
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
就是將arrayMEthods
賦值給value.__proto__
措拇,也就是將處理過的數(shù)組原型上的方法賦值給數(shù)組原型我纪,也就是劫持下數(shù)組原型對象,這樣子我們就可以在調(diào)用[].splice
之類的方法時在不破壞原生的操作之后加上自己的一些操作
copyAugment
就是在數(shù)組不支持__proto__
時,那我們就需要遍歷arrayKeys
浅悉,然后使用def
(Object.defineProperty
)逐項設(shè)置趟据,這樣子也可以達(dá)到類似的效果
這樣子就能做到在調(diào)用.splice
之類的方法時可以執(zhí)行注入到原型上的邏輯
這只是修改了數(shù)組對象的原型對象指向,將其指向修改過的
arrayMethods
术健。也就是并不是所有的數(shù)組對象都會被劫持汹碱,只有被觀測的數(shù)組對象才會被劫持
最后this.observeArray(value)
observeArray(items: Array<any>) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
就是遍歷數(shù)組,然后逐項觀測即可
arrayMethods
接下來看看arrayMethods
對Array.prototype
做了什么改動
const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)
const methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
methodsToPatch.forEach(function (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
})
})
首先我們?nèi)〉綌?shù)組原型對象荞估,然后通過Object.create
復(fù)制一份副本咳促。因?yàn)榻俪直厝粫υ镜淖龀龈膭樱褂酶北镜脑挷粫绊懺镜?br>
然后定義methodsToPatch
變量存儲會對數(shù)組做出改動的方法勘伺,因?yàn)槿舨粫?shù)組做出改動就沒有什么劫持的價值
以上是準(zhǔn)備工作跪腹,接下來遍歷methodsToPatch
。首先取得該項原方法賦值給original
飞醉,然后使用def
覆蓋在原型上的此方法冲茸,既然是劫持,就不好做些影響原本結(jié)果的情況缅帘,就比如push
的結(jié)果秤朗,劫持完了的push
也該和原本的一致
const result = original.apply(this, args)
// ...省略
return result
這個就是調(diào)用原方法得到操作結(jié)果端逼,最后返回
接下來看我們注入的邏輯
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()
首先取得這個數(shù)組對象對應(yīng)的__ob__
賦值給ob
喊熟,然后我們試想下困介,這么些個方法里有幾個可是會增加新元素的召衔,新的值自然也是需要觀測的铃诬,所以我們得拿到這部分新值。對于push苍凛、unshift
趣席,args
就是新增的元素,splice
可新增也可刪除醇蝴,新增的話就是參數(shù)的第三項宣肚,所以取args.slice(2)
。然后簡單了悠栓,判斷inserted
存在的話就調(diào)用ob.observeArray(inserted)
霉涨,最后調(diào)用ob.dep.notify()
,觸發(fā)該數(shù)組對象上收集到的依賴
觀測數(shù)組和觀測對象為何要區(qū)分
我們可以看到數(shù)組和純對象觀測是不一樣的惭适,純對象的話每個鍵值都Object.defineProperty
處理過笙瑟,而數(shù)組的話索引是沒有被處理過的,這也就導(dǎo)致了數(shù)組的索引是非響應(yīng)式的
這個在官網(wǎng)有提到
其實(shí)這里很多人看到會有誤區(qū)癞志,也就是是不是
Object.defineProperty
監(jiān)測不到索引變動什么的往枷,其實(shí)不是。看這個issue8562也就是其實(shí)完全可以當(dāng)做純對象處理错洁,不過終究是
性能代價和獲得的用戶體驗(yàn)收益不成正比
Vue.set
上章節(jié)我們簡單說了下新增屬性原理秉宿,也就是Vue.set
,即:
- 將新屬性值轉(zhuǎn)為響應(yīng)式
- 觸發(fā)新屬性宿主對象收集到的依賴(
__ob__
)
新增我們根據(jù)這個思路來看看Vue.set
源碼
export function set(target: Array<any> | Object, key: any, val: any): any {
if (process.env.NODE_ENV !== 'production' &&
(isUndef(target) || isPrimitive(target))
) {
warn(`Cannot set reactive property on undefined, null, or primitive value: ${(target: 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
}
首先判斷下該宿主對象情況屯碴,不能是undefined描睦、null、原始類型
然后判斷下若是數(shù)組导而,而且key
是有效地索引酌摇,那么直接用splice
就行了
接下來這段有點(diǎn)門道,所以深究下
if (key in target && !(key in Object.prototype)) {
target[key] = val
return val
}
這個if
有倆條件嗡载,即key
在target
或者其原型鏈上且不能在Object.prototype
上窑多,那么就直接修改值就行了。其實(shí)原本并非如此洼滚,詳情看這issues/6845埂息。原本僅僅是if (hasOwn(target, key))
class Model {
constructor() {
this.foo = '123'
this._bar = null
}
get bar() {
return this._bar;
}
set bar(newvalue) {
this._bar = newvalue;
}
}
data = new Model()
試想若是target、key
分別是data遥巴、'bar'
那么hasOwn(data, 'bar') === false
千康、'bar' in data && !('bar' in Object.prototype) === true
可見前者會當(dāng)做新增屬性,后者直接當(dāng)做已有屬性铲掐,直接修改即可拾弃,即觸發(fā)set bar
最后代碼到這了就必然是新增屬性
首先就是簡單的取下ob
對象,然后就是揭示一個規(guī)矩:
- 不能給
Vue
實(shí)例設(shè)置新屬性
這個就是可能出現(xiàn)覆蓋情況 - 不能給根
data
設(shè)置新屬性
這個有點(diǎn)講究摆霉,其實(shí)呢是可以的豪椿,如demo4。它為什么不可以呢携栋,我們知道initData
里有對data
實(shí)現(xiàn)了代理訪問即proxy(vm, '_data', key)
搭盾。也就是vm.a === vm._data.a
。我們新增的自然也就沒有這層代理婉支,那么根數(shù)據(jù)新增屬性自然也就不能vm.nVal
這樣子訪問了鸯隅。所以如例子所示,自行做了這個代理就可以啦
然后要是ob
不存在的話就說明這個target
非響應(yīng)式向挖,簡單設(shè)置即可
最后就是defineReactive
轉(zhuǎn)化成響應(yīng)式蝌以,并且ob.dep.notify()
觸發(fā)依賴更新
Vue.del
export function del(target: Array<any> | Object, key: any) {
if (process.env.NODE_ENV !== 'production' &&
(isUndef(target) || isPrimitive(target))
) {
warn(`Cannot delete reactive property on undefined, null, or primitive value: ${(target: any)}`)
}
if (Array.isArray(target) && isValidArrayIndex(key)) {
target.splice(key, 1)
return
}
const ob = (target: any).__ob__
if (target._isVue || (ob && ob.vmCount)) {
process.env.NODE_ENV !== 'production' && warn(
'Avoid deleting properties on a Vue instance or its root $data ' +
'- just set it to null.'
)
return
}
if (!hasOwn(target, key)) {
return
}
delete target[key]
if (!ob) {
return
}
ob.dep.notify()
}
首先就是和Vue.set
一樣的判定以及數(shù)組情況下調(diào)用劫持過的數(shù)組方法處理還有Vue實(shí)例對象以及根data
不能操作的限定
然后就是if (!hasOwn(target, key)) {
,這個就是判定該對象上有沒有該屬性何之,沒有的話自然就return
跟畅。這里為什么不用和Vue.set
里一樣呢,這是因?yàn)?strong>delete操作只會在自身的屬性上起作用帝美,要刪除原型鏈上的屬性就得傳入那個原型對象
最后就是刪除該屬性碍彭,判斷下ob
不在的話就return
晤硕,因?yàn)椴皇琼憫?yīng)式的自然不用觸發(fā)更新,是的話就ob.dep.notify()
觸發(fā)依賴更新