Object的變化偵測

1.1 什么是變化偵測

vue.js會自動通過狀態(tài)生成DOM悲关,并將其輸出到頁面顯示,這個過程叫渲染。vue.js的渲染過程是生命式的,我們通過模板來描述狀態(tài)和DOM之間的映射關(guān)系叮雳。
通常,在運(yùn)行時應(yīng)用內(nèi)部的狀態(tài)會不斷發(fā)生變化妇汗,此時需要不停的重新渲染帘不,這時候如何確定狀態(tài)發(fā)生了變化?它分為兩種類型:一種是“推”(push)铛纬,另一種是“拉”(pull)
Angular和React中的變化偵測都屬于“拉”厌均,這就是說當(dāng)狀態(tài)發(fā)生變化時,它不知道那個狀態(tài)變了告唆,只知道狀態(tài)有可能變了,然后會發(fā)送一個信號給框架晶密,框架內(nèi)部收到信號后擒悬,會進(jìn)行暴力對比來找出哪些DOM節(jié)點(diǎn)需要重新渲染。這在Angular中是臟檢查的流程稻艰,在React中使用的是虛擬DOM懂牧,
而Vue.js的變化測試屬于“推”,當(dāng)狀態(tài)發(fā)生變化時候,Vue.js立刻就知道僧凤,而且在一定程度上知道哪些狀態(tài)變了畜侦。因此,它知道的信息更多躯保,也就可以進(jìn)行更細(xì)粒度的更新旋膳。
所謂更細(xì)粒度的更新,就是說:假如有一個狀態(tài)綁定了好多個依賴途事,每個依賴表示一個具體的DOM節(jié)點(diǎn)验懊,那么當(dāng)這個狀態(tài)發(fā)生變化時,像這個狀態(tài)的所有依賴發(fā)送通知尸变,讓他們進(jìn)行DOM更新操作义图,相比較而言,“拉”的粒度時最粗的召烂。
但是它也有一定的代價碱工,因?yàn)榱6仍郊?xì),每個狀態(tài)綁定的依賴也就越多奏夫,依賴追蹤在內(nèi)存上的消耗也就越大怕篷,因此,在Vue.js2.0中桶蛔,它引入了虛擬DOM匙头,將粒度調(diào)整為中等粒度,即一個狀態(tài)所綁定的依賴不再是具體的某個DOM節(jié)點(diǎn)仔雷,而是一個組件蹂析,這樣狀態(tài)變化后,會通知到組件碟婆,組件內(nèi)部在使用虛擬DOM進(jìn)行對比电抚,這樣可以降低依賴的數(shù)量,從而降低內(nèi)存消耗竖共。

1.2 如何追蹤變化

1.Object.defineProperty
2.ES6的Proxy

知道Object.defineProperty可以檢測到對象的變化蝙叛,那么我們可以寫出如下代碼:

function defineReactive(data, key, val) {
    Object.defineProperty(data, key, {
        enumerable: true,
        configurable: true,
        get: function () {
            return val;
        },
        set: function (newVal) {
            if (val === newVal){
                return;
            }
            val = newVal;
        }
    })
}

這里的函數(shù)defineDirective用來對Object.defineProperty進(jìn)行封裝,從函數(shù)的名字可以看書公给,其作用是定義一個響應(yīng)式數(shù)據(jù)借帘,也就是在這個函數(shù)中進(jìn)行變化追蹤,封裝后只需要傳遞data淌铐,key肺然,val即可。
封裝好之后每當(dāng)從data的key中讀取數(shù)據(jù)的時候腿准,get被觸發(fā)际起,每當(dāng)往data的key中設(shè)置數(shù)據(jù),set函數(shù)觸發(fā)。

1.3 如何收集依賴街望?

之所以要觀察數(shù)據(jù)校翔,其目的是當(dāng)數(shù)據(jù)的屬性發(fā)生變化是,可以通知那些曾經(jīng)使用了該數(shù)據(jù)的地方灾前。
舉個例子

<template>
    <h1>{{name}}</h1>
</template>

該模板中使用了數(shù)據(jù)name,所以當(dāng)它發(fā)生變化時防症,要向使用它的地方發(fā)送通知
注意:在Vue.js 2.0中,模板使用數(shù)據(jù)等于組件使用數(shù)據(jù)豫柬,所以當(dāng)數(shù)據(jù)發(fā)生變化時告希,會通知發(fā)送到組件,然后組件內(nèi)部在通過虛擬DOM重新渲染烧给。
對于上面的問題燕偶,我的回答是,先收集依賴础嫡,即把用到數(shù)據(jù)name的地方收集起來指么,然后等屬性發(fā)生變化時候,把收集好的依賴循環(huán)觸發(fā)一下榴鼎。
總結(jié)起來就是: 在getter中收集依賴伯诬,在setter中觸發(fā)依賴。

1.4 依賴收集在哪里

現(xiàn)在我們已經(jīng)有了很明確的目標(biāo)巫财,就是在getter中收集依賴盗似,那么要把依賴收集到哪里去?
思考一下平项,說先想到的是每個key都有一個數(shù)組赫舒,用來存儲當(dāng)前key的依賴,假設(shè)依賴時一個函數(shù)闽瓢,保存在window.target上接癌,現(xiàn)在就可以吧defindReactive函數(shù)稍微該著一下:

function defineReactive(data, key, val) {
    let dep = []; // 新增
    Object.defineProperty(data, key, {
        enumerable: true,
        configurable: true,
        get: function () {
            dep.push(window.target); // 新增
            return val;
        },
        set: function (newVal) {
            if (val === newVal){
                return;
            }
            // 新增
            for(let i = 0;i < dep.length; i++){
                dep[i](newVal, val)
            }
            val = newVal;
        }
    })
}

這里我們新增了數(shù)組dep,用來存儲被收集的依賴。
然后在set被觸發(fā)時扣讼,循環(huán)dep以觸發(fā)收集到的依賴缺猛。
這樣寫有點(diǎn)耦合,我們把依賴收集的代碼封裝成Dep類椭符,它專門幫助我們管理依賴徙融。使用這個類灼卢,我們可以收集依賴莱找,刪除依賴赦颇,或者向依賴發(fā)送通知,代碼如下:

export default class Dep{
    constructor() {
        this.subs = [];
    }

    addSub(sub) {
        this.subs.push(sub);
    }

    removeSub(sub) {
        remove(this.subs, sub);
    }

    depend() {
        if(window.target) {
            this.addSub(window.target);
        }
    }

    notify() {
        const subs = this.subs.slice();
        for (let i = 0; i < subs.length ; i++) {
            subs[i].update()
        }
    }
}

function remove(arr, item) {
    if (arr.length) {
        const index = arr.indexOf(item);
        if (index > -1) {
            return arr.splice(index, 1);
        }
    }
}

之后我們再改造一下defineReactive:

function defineReactive(data, key, val) {
    let dep = new Dep(); // 修改
    Object.defineProperty(data, key, {
        enumerable: true,
        configurable: true,
        get: function () {
            dep.depend(); // 修改
            return val;
        },
        set: function (newVal) {
            if (val === newVal){
                return;
            }
            val = newVal;
            dep.notify(); // 修改
        }
    })
}

此時曙搬,代碼收集到Dep中!

1.5 依賴是誰

從上面的代碼中,我們收集的依賴時window.target纵装,那么它到底是什么征讲?我們究竟要收集誰呢?
收集誰橡娄?換句話說诗箍,就是當(dāng)屬性發(fā)生變化后,通知誰挽唉。
我們要通知到用到數(shù)據(jù)的地方滤祖,而是用這個數(shù)據(jù)的地方有很多,而且類型還不一樣瓶籽,即有可能是模板匠童,也有可能是用戶?的一個watch,這是需要抽象出一個能集中處理這些情況的類塑顺,然后我們在依賴收集階段只收集封裝好的類的實(shí)例進(jìn)來汤求,通知也只通知他一個,接著严拒,他在負(fù)責(zé)通知其他地方扬绪。所以,我們要抽象這個動作需要掀起一個名字裤唠,就是Watcher挤牛。
總結(jié):收集誰?Watcher种蘸!

1.6 什么是Watcher

Watcher是一個中介的角色墓赴,數(shù)據(jù)發(fā)生變化時通知它,然后它再通知其他地方劈彪。
關(guān)于Watcher竣蹦,先看一個經(jīng)典的使用方式:

// keypath
vm.$watch('a.b.c', function(newVal, oldVal){
    // todo
})

這段代碼表示當(dāng)data.a.b.c屬性發(fā)生變化時,觸發(fā)第二個參數(shù)中的函數(shù)
怎么實(shí)現(xiàn)這個功能沧奴?好像只要把這個watcher實(shí)例添加到data.a.b.c屬性的Dep中就行痘括,然后當(dāng)data.a.b.c的值發(fā)生變化時,通知Watcher滔吠。接著纲菌,Watcher在執(zhí)行參數(shù)中的回調(diào)函數(shù)。代碼如下:

export default class Watcher{
    constructor(vm, expOrFn, cb){
        this.vm = vm;
        // 執(zhí)行this.getter(),就可以讀取data.a.b.c的內(nèi)容
        this.getter = parsePath(expOrFn)
        this.cb = cb;
        this.value = this.get()
    }

    get() {
        window.target = this;
        let value = this.getter.call(this.vm, this.vm);
        window.target = undefined;
        return value;
    }
    
    update() {
        const oldValue = this.value;
        this.value = this.get();
        this.cb.call(this.vm, this.value, oldValue);
    }
}

這段代碼可以把自己主動添加到data.a.b.c的Dep中去疮绷,是不是很神奇翰舌?
因?yàn)樵趃et方法中先把window.target設(shè)置成了this,也就是當(dāng)前watcher實(shí)例,然后在讀一下data.a.b.c的值冬骚,這肯定會觸發(fā)getter椅贱。
觸發(fā)了getter懂算,就會觸發(fā)收集依賴的邏輯,上面已經(jīng)介紹了庇麦,會從window.target中讀取一個依賴并添加到Dep中计技。
這就導(dǎo)致,只要現(xiàn)在window.target賦一個this,然后再讀一下值山橄,去觸發(fā)getter,就可以把this主動添加到keypath中的Dep垮媒。
依賴注入到Dep中后,每當(dāng)data.a.b.c的值發(fā)生變化時航棱,就會讓依賴列表中所欲的依賴循環(huán)觸發(fā)update方法睡雇,也就是Watcher中的update方法,而update方法會執(zhí)行參數(shù)中回調(diào)函數(shù)饮醇,將value和oldValue傳到參數(shù)中它抱。
所以,其實(shí)不管是用戶執(zhí)行vm.$watch('a.b.c'驳阎,(value, oldValue) => {}),還是模板中用到的data,都是通過Watcher來通知自己是否需要發(fā)生變化抗愁。

/**
 * 解析簡單路徑
 * */
const bailRe = /[^\w.$]/
export function parsePath(path) {
    if(bailRe.test(path)){
        return;
    }
    const segments = path.split('.')
    return function (obj) {
        for(let i = 0; i < segments.length; i++) {
            if (!obj) {
                return;
            }
            obj = obj[segments[i]];
        }
        return obj;
    }
}

可以看到,這其實(shí)并不復(fù)雜呵晚,現(xiàn)將keypath蜘腌,用.分割成數(shù)組,然后循環(huán)數(shù)組一層一層去讀取數(shù)據(jù)饵隙,最后拿到的obj就是keypath中想要的數(shù)據(jù)撮珠。

1.7 遞歸偵測所有key

現(xiàn)在其實(shí)已經(jīng)可以實(shí)現(xiàn)變化偵測的功能了,但是前面介紹的代碼只能偵測數(shù)據(jù)的某一個屬性金矛,我們希望吧數(shù)據(jù)中的所有屬性包括子屬性都偵測到芯急,所以要封裝一個Observer類,這個類的作用是將一個數(shù)據(jù)內(nèi)的所有屬性包括子屬性都轉(zhuǎn)化成getter/setter的形式驶俊,然后去追蹤他們的變化:

/**
 * Observer類會附加到每一個被偵測的object上
 * 一旦被附加上娶耍,Observer會將object的所有屬性轉(zhuǎn)化為getter/setter的形式
 * 來收集屬性的依賴,并且當(dāng)屬性發(fā)生變化時會通知這些依賴
 * */
export class Observer{
    constructor(value) {
        this.value = value;
        if (!Array.isArray(value)){
            this.walk(value)
        }
    }
    
    /**
     * walk會將每一個屬性都轉(zhuǎn)化為getter/setter的形式來偵測變化
     * 這個方法只有在數(shù)據(jù)類型為Object時候被調(diào)用
     * */
    walk(obj) {
        const keys = Object.keys(obj);
        for (let i = 0; i < keys.length; i++) {
            defineReactive(obj, keys[i], obj[keys[i]])
        }
    }
}

function defineReactive(data, key, val) {
    // 新增饼酿,遞歸子屬性
    if (typeof val === 'object') {
        new Observer(val);
    }
    let dep = new Dep(); // 修改
    Object.defineProperty(data, key, {
        enumerable: true,
        configurable: true,
        get: function () {
            dep.depend(); // 修改
            return val;
        },
        set: function (newVal) {
            if (val === newVal){
                return;
            }
            val = newVal;
            dep.notify(); // 修改
        }
    })
}

從上面代碼中榕酒,我們定義了Observer類,讓他用來講一個正常的object轉(zhuǎn)化為被偵測的object故俐,然后去判斷你數(shù)據(jù)的類型想鹰,只有object數(shù)據(jù)類型才會調(diào)用getter/setter的形式來偵測變化,最后在defineReactive中新增new Observer(val)來遞歸子屬性药版,這樣我們就可以把data中的所有屬性(包括子屬性)都轉(zhuǎn)化為getter/setter的形式來偵測變化
當(dāng)data中的屬性發(fā)生變化時辑舷,與這個屬性對應(yīng)得依賴就會收到通知,也就是說我們將一個object傳到Observer中槽片,那么這個object就會變成響應(yīng)式的object何缓。

1.8 關(guān)于Object的問題

前面介紹了object類型的變化偵測原理肢础,了解了數(shù)據(jù)變化時通過getter/setter來追蹤變化的,也正是由于這種追蹤方式歌殃,有些語法即便發(fā)生了變化乔妈,Vue.js也追蹤不到,比如像object添加屬性:

var vm = new Vue({
    el: '#el',
    template: '#demo',
    methods: {
      action() {
          this.obj.name = 'test';
      }  
    },
    data: {
        obj: {}
    }
})

在action方法中氓皱,我們在obj上面新增了name屬性,Vue.js無法偵測到這個變化勃刨,所以不會向依賴發(fā)送通知

var vm = new Vue({
    el: '#el',
    template: '#demo',
    methods: {
        action() {
            delete this.obj.name
        }
    },
    data: {
        obj: {
            name: 'test'
        }
    }
})

在action方法中波材,我們在obj上面刪除了name屬性,Vue.js無法偵測到這個變化身隐,所以不會向依賴發(fā)送通知
Vue.js 通過Object.defineProperty來將對象的key轉(zhuǎn)化成getter/setter的形式追蹤變化廷区,只能追蹤到是否被修改,無法追蹤到新屬性和刪除屬性贾铝,所以vue.js提供了兩個api隙轻,vm.set和vm.delete。

1.9總結(jié)

1.變化偵測就是偵測數(shù)據(jù)的變化垢揩,當(dāng)數(shù)據(jù)發(fā)生變化時玖绿,要能偵測到并發(fā)出通知。
2.Object可以通過Object.defineProperty將屬性轉(zhuǎn)化為getter/setter的形式來追蹤變化叁巨,讀取數(shù)據(jù)時用getter斑匪,修改數(shù)據(jù)時用setter。
3.我們需要在getter中收集哪些依賴使用了數(shù)據(jù)锋勺,當(dāng)setter被觸發(fā)時蚀瘸,就去通知getter中收集的依賴數(shù)據(jù)發(fā)生變化。
4.收集依賴需要為依賴找一個存儲的地方庶橱,為此我們創(chuàng)建了Dep,它用來收集依賴贮勃,刪除依賴和向依賴發(fā)送通知。
5.所謂的依賴苏章,其實(shí)就是Watcher寂嘉。只有Watcher觸發(fā)的getter才會收集依賴,哪個Watcher觸發(fā)了getter布近,就把哪個Watcher收集到Dep中垫释。當(dāng)數(shù)據(jù)發(fā)生變化時,會循環(huán)依賴列表撑瞧,把所有的Watcher都通知一遍.
Watcher的原理就是先把自己設(shè)置到全局唯一的指定位置棵譬,例如window.target。然后讀取數(shù)據(jù)预伺,因?yàn)樽x取了數(shù)據(jù)订咸,所以會觸發(fā)數(shù)據(jù)的getter,在getter中就會從全局唯一的那個位置讀當(dāng)前正在讀取數(shù)據(jù)的Watcher曼尊,并把這個Watcher收集到Dep中去,通過這樣的方式脏嚷,Watcher可以主動的去訂閱任意一個數(shù)據(jù)的變化骆撇。
6.此外,我們創(chuàng)建了Observer類父叙,它的作用是吧一個object中的數(shù)據(jù)神郊,包括子數(shù)據(jù)都轉(zhuǎn)化成響應(yīng)式的,也就是他會偵測object的所有數(shù)據(jù)的變化趾唱。

文章出自vue.js深入淺出一書

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末涌乳,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子甜癞,更是在濱河造成了極大的恐慌夕晓,老刑警劉巖,帶你破解...
    沈念sama閱讀 212,383評論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件悠咱,死亡現(xiàn)場離奇詭異蒸辆,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)析既,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,522評論 3 385
  • 文/潘曉璐 我一進(jìn)店門躬贡,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人渡贾,你說我怎么就攤上這事逗宜。” “怎么了空骚?”我有些...
    開封第一講書人閱讀 157,852評論 0 348
  • 文/不壞的土叔 我叫張陵纺讲,是天一觀的道長。 經(jīng)常有香客問我囤屹,道長熬甚,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,621評論 1 284
  • 正文 為了忘掉前任肋坚,我火速辦了婚禮乡括,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘智厌。我一直安慰自己诲泌,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,741評論 6 386
  • 文/花漫 我一把揭開白布铣鹏。 她就那樣靜靜地躺著敷扫,像睡著了一般。 火紅的嫁衣襯著肌膚如雪诚卸。 梳的紋絲不亂的頭發(fā)上葵第,一...
    開封第一講書人閱讀 49,929評論 1 290
  • 那天绘迁,我揣著相機(jī)與錄音,去河邊找鬼卒密。 笑死缀台,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的哮奇。 我是一名探鬼主播膛腐,決...
    沈念sama閱讀 39,076評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼屏镊!你這毒婦竟也來了依疼?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,803評論 0 268
  • 序言:老撾萬榮一對情侶失蹤而芥,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后膀值,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體棍丐,經(jīng)...
    沈念sama閱讀 44,265評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,582評論 2 327
  • 正文 我和宋清朗相戀三年沧踏,在試婚紗的時候發(fā)現(xiàn)自己被綠了歌逢。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,716評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡翘狱,死狀恐怖秘案,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情潦匈,我是刑警寧澤阱高,帶...
    沈念sama閱讀 34,395評論 4 333
  • 正文 年R本政府宣布,位于F島的核電站茬缩,受9級特大地震影響赤惊,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜凰锡,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 40,039評論 3 316
  • 文/蒙蒙 一未舟、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧掂为,春花似錦裕膀、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,798評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至智绸,卻和暖如春野揪,著一層夾襖步出監(jiān)牢的瞬間访忿,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,027評論 1 266
  • 我被黑心中介騙來泰國打工斯稳, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留海铆,地道東北人。 一個月前我還...
    沈念sama閱讀 46,488評論 2 361
  • 正文 我出身青樓挣惰,卻偏偏與公主長得像卧斟,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子憎茂,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,612評論 2 350

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