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

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

vue官方文檔中有一目---”深入響應(yīng)式原理“,專門闡述了這一特性的實(shí)現(xiàn)機(jī)制和媳,然而篇幅有限格遭,有些具體點(diǎn)的闡述對初學(xué)者來講還是不是很友好。前一段時(shí)間自己專門去找了一些源碼相關(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)聽到一個(gè)數(shù)據(jù)的變化的硬梁,了解這一點(diǎn)是理解”響應(yīng)式“機(jī)制的第一步前硫。

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

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

 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)對視圖層的每一個(gè)值進(jìn)行相應(yīng)的改變淋昭。上一小節(jié)的響應(yīng)式雛形代碼中只對在setter中承擔(dān)了相應(yīng)過程中的部分功能俐填。而getter則完成了另一部分的功能,即收集視圖層的數(shù)據(jù)依賴翔忽。這里的依賴準(zhǔn)確來說應(yīng)該叫做一個(gè)觀察者英融,它負(fù)責(zé)監(jiān)測一對相互關(guān)聯(lián)的數(shù)據(jù)和引用該數(shù)據(jù)的視圖,并維護(hù)著新數(shù)據(jù)渲染的方法歇式。

vue響應(yīng)式系統(tǒng)實(shí)現(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)文檔上的這個(gè)原理圖就要清楚的多,其中“touch”的過程就是渲染視圖時(shí)讀數(shù)據(jù)觸發(fā)getter的過程秸弛,而“wathcer”就是上文說的觀察者铭若,它具體是怎樣的實(shí)現(xiàn)將在之后的小節(jié)中進(jìn)行具體說明。


image

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

2.1 訂閱者Dep

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


image.png

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

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

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

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

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)一步封裝為一個(gè)函數(shù)甸饱,并補(bǔ)充完整其getter的內(nèi)容,這里每個(gè)帶觀測數(shù)據(jù)和每個(gè)dep實(shí)例是一一對應(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ì)為一個(gè)對象柜候,我們在組件實(shí)例中定義的watch的成員就是在為數(shù)據(jù)綁定一個(gè)個(gè)的watcher,視圖部分有可能是dom中一個(gè)元素屬性或文本節(jié)點(diǎn)躏精,不同形式的視圖層其更新方式有所不同渣刷。在響應(yīng)式的環(huán)節(jié)中,每個(gè)觀察者存有對應(yīng)視圖的更新方法矗烛。

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

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

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

class Watcher {
    constructor (vm,expOrFunc,cb) {
        //vm vue實(shí)例
        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)行實(shí)例化并簡單的模擬響應(yīng)式數(shù)據(jù)。由于代碼中未加入html模板編譯的過程塔淤,這里僅用js定義watch的形式來產(chǎn)生一個(gè)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實(shí)例構(gòu)建的時(shí)候,會調(diào)用observer函數(shù)對data對象中的每個(gè)屬性進(jìn)行響應(yīng)式化高蜂,即定義他們的getter和setter并初始化每個(gè)屬性對應(yīng)的dep實(shí)例第焰。同時(shí)根據(jù)配置屬性watch來生成一個(gè)針對屬性a的watcher,每當(dāng)這個(gè)數(shù)據(jù)發(fā)生變化時(shí)妨马,將調(diào)用回調(diào)函數(shù)更新視圖(這里只是控制臺輸出)挺举。說明起見,每個(gè)響應(yīng)式屬性在setter中執(zhí)行完屬性收集烘跺,將打印一下對應(yīng)的dep.subs湘纵。代碼執(zhí)行,控制臺打印如下:


image.png

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

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


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是因?yàn)樵谡嬲乱晥D前霎烙,需要調(diào)用get函數(shù)去讀取一下新值,所以又觸發(fā)了一次setter蕊连。由于我們做了防止watcher重復(fù)注冊的判斷悬垃,故打印出的dep.subs中依然只有id為0的一個(gè)watcher。

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

然而盗忱,還有一個(gè)問題值得思考酱床,vue中我們觀察的很可能是個(gè)對象羊赵,比如a.name、a.name.first這樣,當(dāng)對象內(nèi)部的值發(fā)生改變昧捷,視圖依然可以發(fā)生改變闲昭。做到這樣的深度觀察,即需要為對象內(nèi)部的值也定義好其setter及getter靡挥,實(shí)現(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的值改為一個(gè)數(shù)據(jù)再執(zhí)行下上面的過程:

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末跋破,一起剝皮案震驚了整個(gè)濱河市簸淀,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌毒返,老刑警劉巖租幕,帶你破解...
    沈念sama閱讀 206,378評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異拧簸,居然都是意外死亡劲绪,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,356評論 2 382
  • 文/潘曉璐 我一進(jìn)店門盆赤,熙熙樓的掌柜王于貴愁眉苦臉地迎上來贾富,“玉大人,你說我怎么就攤上這事牺六〔梗” “怎么了?”我有些...
    開封第一講書人閱讀 152,702評論 0 342
  • 文/不壞的土叔 我叫張陵淑际,是天一觀的道長汇鞭。 經(jīng)常有香客問我,道長庸追,這世上最難降的妖魔是什么霍骄? 我笑而不...
    開封第一講書人閱讀 55,259評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮淡溯,結(jié)果婚禮上读整,老公的妹妹穿的比我還像新娘。我一直安慰自己咱娶,他們只是感情好米间,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,263評論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著膘侮,像睡著了一般屈糊。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上琼了,一...
    開封第一講書人閱讀 49,036評論 1 285
  • 那天逻锐,我揣著相機(jī)與錄音夫晌,去河邊找鬼。 笑死昧诱,一個(gè)胖子當(dāng)著我的面吹牛晓淀,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播盏档,決...
    沈念sama閱讀 38,349評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼凶掰,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了蜈亩?” 一聲冷哼從身側(cè)響起懦窘,我...
    開封第一講書人閱讀 36,979評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎稚配,沒想到半個(gè)月后奶赠,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,469評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡药有,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,938評論 2 323
  • 正文 我和宋清朗相戀三年毅戈,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片愤惰。...
    茶點(diǎn)故事閱讀 38,059評論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡苇经,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出宦言,到底是詐尸還是另有隱情扇单,我是刑警寧澤,帶...
    沈念sama閱讀 33,703評論 4 323
  • 正文 年R本政府宣布奠旺,位于F島的核電站蜘澜,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏响疚。R本人自食惡果不足惜鄙信,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,257評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望忿晕。 院中可真熱鬧装诡,春花似錦、人聲如沸践盼。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,262評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽咕幻。三九已至渔伯,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間肄程,已是汗流浹背锣吼。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工选浑, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人吐限。 一個(gè)月前我還...
    沈念sama閱讀 45,501評論 2 354
  • 正文 我出身青樓鲜侥,卻偏偏與公主長得像褂始,于是被迫代替她去往敵國和親诸典。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,792評論 2 345