Vue響應(yīng)式系統(tǒng)原理分析與簡單實現(xiàn)(上)

對于VUE,最顯著的特點之一就是其數(shù)據(jù)雙向綁定而帶來的奇妙開發(fā)體驗。經(jīng)由vue源碼中的某些操作,使得工程師在項目開發(fā)過程中,無需操作Dom,邏輯層對數(shù)據(jù)的改變便會自動反饋在視圖層;反過來,v-model的使用也會使得用戶在視圖層上的修改映射到真實數(shù)據(jù)上。

vue官方文檔中有一目---”深入響應(yīng)式原理“,專門闡述了這一特性的實現(xiàn)機(jī)制,然而篇幅有限,有些具體點的闡述對初學(xué)者來講還是不是很友好。前一段時間自己專門去找了一些源碼相關(guān)的內(nèi)容去學(xué)習(xí),詳細(xì)了解了一下這一過程。此篇文章將會梳理總結(jié)一下自己的學(xué)習(xí)成果。完整代碼見:https://github.com/cyanl77/mvvm

下面進(jìn)入正題。(持續(xù)更新)

1 概述

1.1 數(shù)據(jù)變化監(jiān)聽

”深入響應(yīng)式原理“第一小節(jié)叫做”如何追蹤變化“,它想要探討的問題和此部分一致,即javascript本身是如何監(jiān)聽到一個數(shù)據(jù)的變化的,了解這一點是理解”響應(yīng)式“機(jī)制的第一步。

實現(xiàn)這一功能的是Object.defineProperty。該方法本身的目的在于定義或修改一個對象的現(xiàn)有屬性,該方法第三個參數(shù)屬性描述符可通過一對函數(shù)getter和setter來定義一個屬性的存取特性,它們分別在該屬性被讀取或重新賦值的時候被調(diào)用?,F(xiàn)在可明確,js即是通過定義待觀測屬性的getter和setter來達(dá)到監(jiān)測其變化,進(jìn)而響應(yīng)變化的目的。

到此,可以寫出如下實現(xiàn)響應(yīng)式系統(tǒng)的雛形,假設(shè)我們要監(jiān)測一個對象中屬性,當(dāng)其發(fā)生改變時,自動在控制臺輸出:

 Object.defineProperty(obj, key, {
        enumerable: true,
        configurable: true,
        get () {
            //do something
            return value;
        },
        set (newValue) {
            if(value !== newValue) {
                value = newValue;
                //do something
            }
        }
    })

1.2 vue中的數(shù)據(jù)依賴收集

vue構(gòu)建的視圖中,可能有多處依賴于data中的同一屬性,當(dāng)邏輯層的值發(fā)生變化,理應(yīng)對視圖層的每一個值進(jìn)行相應(yīng)的改變。上一小節(jié)的響應(yīng)式雛形代碼中只對在setter中承擔(dān)了相應(yīng)過程中的部分功能。而getter則完成了另一部分的功能,即收集視圖層的數(shù)據(jù)依賴。這里的依賴準(zhǔn)確來說應(yīng)該叫做一個觀察者,它負(fù)責(zé)監(jiān)測一對相互關(guān)聯(lián)的數(shù)據(jù)和引用該數(shù)據(jù)的視圖,并維護(hù)著新數(shù)據(jù)渲染的方法。

vue響應(yīng)式系統(tǒng)實現(xiàn)原理到此已經(jīng)大致清晰:為data中所有屬性綁定其存取屬性getter和setter,其中,getter用來收集,setter用來更新。每當(dāng)視圖層對數(shù)據(jù)進(jìn)行讀取,則調(diào)用getter,將對應(yīng)依賴收集起來;每當(dāng)邏輯層改變該數(shù)據(jù),則調(diào)用setter函數(shù),依次更新收集到的所有依賴。

下面再重新審視官網(wǎng)文檔上的這個原理圖就要清楚的多,其中“touch”的過程就是渲染視圖時讀數(shù)據(jù)觸發(fā)getter的過程,而“wathcer”就是上文說的觀察者,它具體是怎樣的實現(xiàn)將在之后的小節(jié)中進(jìn)行具體說明。


image

2 關(guān)鍵數(shù)據(jù)結(jié)構(gòu)

2.1 訂閱者Dep

訂閱者Dep本質(zhì)是一個類,其功能簡單說就是一個收集管理處。我們知道,對于vue組件實例data中的某一數(shù)據(jù),可能被視圖層多處依賴,每一處依賴,就有一個對應(yīng)的觀察者watcher來負(fù)責(zé)執(zhí)行視圖的變化更新。所以為了在數(shù)據(jù)變化時更新到所有的視圖層數(shù)據(jù),對于每一個數(shù)據(jù),我們都需要維護(hù)這樣一個數(shù)據(jù)結(jié)構(gòu)Dep來收集所有引用該數(shù)據(jù)的watcher,以使得數(shù)據(jù)變化時,它能一一通知收集到的watcher去執(zhí)行對應(yīng)的更新函數(shù)。dep與watcher的關(guān)系如下圖所示:


image.png

具體來說,訂閱者對象實例承擔(dān)了以下工作:

  • 收集watcher。
  • 存儲watcher。
  • 數(shù)據(jù)更新時,循環(huán)通知所有watcher更新對應(yīng)視圖。

這里值得提及一下Dep實例收集觀察者的過程,源碼中采取了巧妙的方式使得一個watcher一旦被實例化,便自己將自己加入對應(yīng)的dep中。其具體過程如下:
1). Dep類自身定義了靜態(tài)變量target,指向新new出的watcher。
2).watcher在構(gòu)造函數(shù)中會為了保存當(dāng)前值(以便待觀察數(shù)據(jù)被賦予新值時進(jìn)行比較)而讀取數(shù)據(jù)。
3).觸發(fā)該數(shù)據(jù)的getter,而每個數(shù)據(jù)的getter中會調(diào)用對應(yīng)dep的收集函數(shù)將target所指向的watcher實例存儲起來。
4).解除target指向直到有新的watcher被實例化出來。

基于以上所述,可封裝如下訂閱者對象:

let depId = 0;
class Dep {
    constructor() {
        this.id = depId++;

        //存儲watcher
        this.subs = [];
    }

    //添加watcher
    addSub(watcher) {
        this.subs.push(watcher);
    }

    depend(){
        Dep.target.addDep(this);
    }

    //數(shù)據(jù)變化,通知所有觀察者更新對應(yīng)視圖
    notify() {
        this.subs.forEach(watcher =>{
            //依賴更新視圖
            watcher.update();
        })
    }
}
Dep.target = null;

數(shù)據(jù)綁定存取屬性的過程也進(jìn)一步封裝為一個函數(shù),并補(bǔ)充完整其getter的內(nèi)容,這里每個帶觀測數(shù)據(jù)和每個dep實例是一一對應(yīng)的關(guān)系:

function defineReactive (obj,key,value) {
    const dep = new Dep();
    Object.defineProperty(obj, key, {
        enumerable: true,
        configurable: true,
        get () {
            if(Dep.target){
                dep.depend();
            }
            return value;
        },
        set (newValue) {
            if(value !== newValue) {
                value = newValue;
                dep.notify();
            }
        }
    })
}

function observer(obj) {
    Object.keys(obj).forEach((key) => {
        defineReactive(obj,key,obj[key]);
        if(typeof obj[key] === 'object') {
            observer(obj[key]);
        }
    })
}

2.2 觀察者watcher

觀察者watcher,其本質(zhì)為一個對象,我們在組件實例中定義的watch的成員就是在為數(shù)據(jù)綁定一個個的watcher,視圖部分有可能是dom中一個元素屬性或文本節(jié)點,不同形式的視圖層其更新方式有所不同。在響應(yīng)式的環(huán)節(jié)中,每個觀察者存有對應(yīng)視圖的更新方法。

由上節(jié)我們知道,當(dāng)一個數(shù)據(jù)在邏輯層發(fā)生改變,會首先通知給watcher的收集管理處Dep,在由Dep一一傳達(dá)收集的watcher,此時每個watcher調(diào)用對應(yīng)的更新方法去更新視圖。具體來說,watcher在這一過程中做了以下工作:

  • 首次實例化,將自己注冊在訂閱者Dep中。
  • 解析待觀察的表達(dá)式,在data中獲取對應(yīng)的新值,存儲舊值。
  • 比較新舊值,當(dāng)新舊值不同,調(diào)用更新方法更新視圖。

基于以上所述, watcher類定義的框架大致如下:

class Watcher {
    constructor (vm,expOrFunc,cb) {
        //vm vue實例
        this.vm = vm;

        // 被觀察的屬性變量名稱
        this.exp = expOrFunc;
        this.getter = function(vm, exp){
            return vm.$data[exp];
        };
        this.id = watcherId++;

        //屬性賦新值后調(diào)用回調(diào)
        this.cb = cb;
        this.deps = [];
        this.value = this.get(); //獲取老值
    }

    get(){
        Dep.target = this;
        let value = this.getter(this.vm,this.exp);
        
        //配合getter中Dep.target非空判斷防止相同watcher二次加入,讀后需解綁
        Dep.target = null;
        return value;
    }

    //注冊
    addDep(dep){
        if(this.deps.indexOf(dep.id) === -1){
            this.deps.push(dep.id);
            dep.addSub(this);
        }
    }

    //對外暴露的方法
    update(){
        let value = this.get(); //新值
        if(this.value !== value) {
            const oldValue = this.value;
            this.value = value;
            this.cb.call(this.vm,value,oldValue);
        }
    }
}

2.3 執(zhí)行

不考慮各種各樣的邊界情況,到這里我們關(guān)鍵數(shù)據(jù)結(jié)構(gòu)已經(jīng)構(gòu)建完全,可以進(jìn)行實例化并簡單的模擬響應(yīng)式數(shù)據(jù)。由于代碼中未加入html模板編譯的過程,這里僅用js定義watch的形式來產(chǎn)生一個watcher觀察數(shù)據(jù), 回調(diào)函數(shù)在控制臺打印更新后的數(shù)據(jù)。具體代碼如下:

class Vue {
    constructor(options){
        this._data = options.data;
        observer(this._data);
        if(options.watch) {
            Object.keys(this._data).forEach((key)=>{
                const watcher = new Watcher(this,key,options.watch[key]);
            })
        }
    }
}

let o = new Vue({
    data: {
        a: 10,
        b: 'hhhh'
    },
    watch: {
        'a': function (newValue) {
            console.log("update a:"+newValue)
        },
        'b': function (newValue) {
            console.log("update b:"+newValue)
        }
    }
})

在vue實例構(gòu)建的時候,會調(diào)用observer函數(shù)對data對象中的每個屬性進(jìn)行響應(yīng)式化,即定義他們的getter和setter并初始化每個屬性對應(yīng)的dep實例。同時根據(jù)配置屬性watch來生成一個針對屬性a的watcher,每當(dāng)這個數(shù)據(jù)發(fā)生變化時,將調(diào)用回調(diào)函數(shù)更新視圖(這里只是控制臺輸出)。說明起見,每個響應(yīng)式屬性在setter中執(zhí)行完屬性收集,將打印一下對應(yīng)的dep.subs。代碼執(zhí)行,控制臺打印如下:


image.png

控制臺輸出兩個watcher的數(shù)組,由于數(shù)據(jù)a,b各自僅擁有一個觀察者watcher,因此每個數(shù)組長度均為1,id分別為0和1。屬性deps解釋一下,該屬性維護(hù)了其已注冊了的訂閱者實例dep的id,一旦watcher的注冊函數(shù)addDep被調(diào)用,其首先會從屬性deps中查看其在這個dep中是否已被注冊過,如果是,則不重新注冊。

當(dāng)改變某個響應(yīng)式屬性,會在賦值時在控制臺打印新值:


image.png

在控制臺改變一下數(shù)據(jù)a的值,觸發(fā)了a所綁定的setter,從而讓a的訂閱者去
通知其subs中所有的watcher調(diào)用update方法去更新視圖,最終調(diào)用了傳給watcher的回調(diào)函數(shù),在控制臺打印“update a:70”。這里還會打印了dep.subs是因為在真正更新視圖前,需要調(diào)用get函數(shù)去讀取一下新值,所以又觸發(fā)了一次setter。由于我們做了防止watcher重復(fù)注冊的判斷,故打印出的dep.subs中依然只有id為0的一個watcher。

(其實我也疑惑為什么不在setter中直接傳值newValue不就無需觸發(fā)getter了嘛,還有為什么watcher加入dep的行為不直接在dep中push了還兜那么大圈子... 也許是這樣的寫法解耦的比較徹底...)

然而,還有一個問題值得思考,vue中我們觀察的很可能是個對象,比如a.name、a.name.first這樣,當(dāng)對象內(nèi)部的值發(fā)生改變,視圖依然可以發(fā)生改變。做到這樣的深度觀察,即需要為對象內(nèi)部的值也定義好其setter及getter,實現(xiàn)方法不難,無非是遞歸,這里用Array的reduce方法來改變一下watcher中讀取數(shù)據(jù)的方法getter:

this.getter = function(vm, exp){
      let exprArr = exp.split('.');
      let value = exprArr.reduce((prev,next) => {
          return prev[next];
      }, vm.$data)
      return value;
  };

將a的值改為一個數(shù)據(jù)再執(zhí)行下上面的過程:

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容