響應(yīng)式系統(tǒng)(三)

前言

這章節(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浅悉,然后使用defObject.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

接下來看看arrayMethodsArray.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有倆條件嗡载,即keytarget或者其原型鏈上且不能在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ā)依賴更新

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末庇忌,一起剝皮案震驚了整個濱河市舞箍,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌皆疹,老刑警劉巖疏橄,帶你破解...
    沈念sama閱讀 206,482評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異略就,居然都是意外死亡捎迫,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,377評論 2 382
  • 文/潘曉璐 我一進(jìn)店門表牢,熙熙樓的掌柜王于貴愁眉苦臉地迎上來窄绒,“玉大人,你說我怎么就攤上這事崔兴≌玫迹” “怎么了?”我有些...
    開封第一講書人閱讀 152,762評論 0 342
  • 文/不壞的土叔 我叫張陵敲茄,是天一觀的道長位谋。 經(jīng)常有香客問我,道長堰燎,這世上最難降的妖魔是什么掏父? 我笑而不...
    開封第一講書人閱讀 55,273評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮秆剪,結(jié)果婚禮上赊淑,老公的妹妹穿的比我還像新娘。我一直安慰自己鸟款,他們只是感情好膏燃,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,289評論 5 373
  • 文/花漫 我一把揭開白布茂卦。 她就那樣靜靜地躺著何什,像睡著了一般。 火紅的嫁衣襯著肌膚如雪等龙。 梳的紋絲不亂的頭發(fā)上处渣,一...
    開封第一講書人閱讀 49,046評論 1 285
  • 那天,我揣著相機(jī)與錄音蛛砰,去河邊找鬼罐栈。 笑死,一個胖子當(dāng)著我的面吹牛泥畅,可吹牛的內(nèi)容都是我干的荠诬。 我是一名探鬼主播,決...
    沈念sama閱讀 38,351評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼柑贞!你這毒婦竟也來了方椎?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,988評論 0 259
  • 序言:老撾萬榮一對情侶失蹤钧嘶,失蹤者是張志新(化名)和其女友劉穎棠众,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體有决,經(jīng)...
    沈念sama閱讀 43,476評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡闸拿,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,948評論 2 324
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了书幕。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片新荤。...
    茶點(diǎn)故事閱讀 38,064評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖台汇,靈堂內(nèi)的尸體忽然破棺而出迟隅,到底是詐尸還是另有隱情,我是刑警寧澤励七,帶...
    沈念sama閱讀 33,712評論 4 323
  • 正文 年R本政府宣布智袭,位于F島的核電站,受9級特大地震影響掠抬,放射性物質(zhì)發(fā)生泄漏吼野。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,261評論 3 307
  • 文/蒙蒙 一两波、第九天 我趴在偏房一處隱蔽的房頂上張望瞳步。 院中可真熱鬧,春花似錦腰奋、人聲如沸单起。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,264評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽嘀倒。三九已至,卻和暖如春局冰,著一層夾襖步出監(jiān)牢的瞬間测蘑,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,486評論 1 262
  • 我被黑心中介騙來泰國打工康二, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留碳胳,地道東北人。 一個月前我還...
    沈念sama閱讀 45,511評論 2 354
  • 正文 我出身青樓沫勿,卻偏偏與公主長得像挨约,于是被迫代替她去往敵國和親味混。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,802評論 2 345

推薦閱讀更多精彩內(nèi)容