Vue數(shù)據(jù)的雙向綁定

一贱除、簡介

每當(dāng)被問到Vue數(shù)據(jù)雙向綁定原理的時候塘揣,大家可能都會脫口而出:Vue內(nèi)部通過Object.defineProperty方法屬性攔截的方式屠阻,把data對象里每個數(shù)據(jù)的讀寫轉(zhuǎn)化成getter/setter馆纳,當(dāng)數(shù)據(jù)變化時通知視圖更新敌厘。雖然一句話把大概原理概括了滴须,但是其內(nèi)部的實現(xiàn)方式還是值得深究的舌狗,本文就以通俗易懂的方式剖析Vue內(nèi)部雙向綁定原理的實現(xiàn)過程。

二扔水、思路分析

所謂MVVM數(shù)據(jù)雙向綁定痛侍,即主要是:數(shù)據(jù)變化更新視圖,視圖變化更新數(shù)據(jù)魔市。如下圖:
MVVM過程

也就是說:

  • 輸入框內(nèi)容變化時主届,data 中的數(shù)據(jù)同步變化。即 view => model 的變化待德。
  • data 中的數(shù)據(jù)變化時君丁,文本節(jié)點的內(nèi)容同步變化。即 model => view 的變化将宪。

要實現(xiàn)這兩個過程绘闷,關(guān)鍵點在于數(shù)據(jù)變化如何更新視圖,因為視圖變化更新數(shù)據(jù)我們可以通過事件監(jiān)聽的方式來實現(xiàn)较坛。所以我們著重討論數(shù)據(jù)變化如何更新視圖印蔗。

數(shù)據(jù)變化更新視圖的關(guān)鍵點則在于我們?nèi)绾沃罃?shù)據(jù)發(fā)生了變化,只要知道數(shù)據(jù)在什么時候變了丑勤,那么問題就變得迎刃而解华嘹,我們只需在數(shù)據(jù)變化的時候去通知視圖更新即可。

三法竞、觀測數(shù)據(jù)對象

數(shù)據(jù)的每次讀和寫能夠被我們看的見耙厚,即我們能夠知道數(shù)據(jù)什么時候被讀取了或數(shù)據(jù)什么時候被改寫了,我們將其稱為數(shù)據(jù)變的‘可觀測’岔霸。

要將數(shù)據(jù)變的‘可觀測’薛躬,我們就要借助前言中提到的Object.defineProperty方法了,關(guān)于該方法秉剑,MDN上是這么介紹的:

Object.defineProperty() 方法會直接在一個對象上定義一個新屬性泛豪,或者修改一個對象的現(xiàn)有屬性, 并返回這個對象侦鹏。

在本文中诡曙,我們就使用這個方法使數(shù)據(jù)變得“可觀測”。

首先舷夺,我們定義一個數(shù)據(jù)對象car

let car = { 'brand':'BMW',  'price':3000 }

我們定義了這個car的品牌brandBMW,價格price是3000≌洳撸現(xiàn)在我們可以通過car.brandcar.price直接讀寫這個car對應(yīng)的屬性值牡属。但是,當(dāng)這個car的屬性被讀取或修改時慎璧,我們并不知情。那么應(yīng)該如何做才能夠讓car主動告訴我們跨释,它的屬性被修改了呢胸私?

接下來,我們使用Object.defineProperty()改寫上面的例子:

let car = {}
let val = 3000
Object.defineProperty(car, 'price', {
        get(){
            console.log('price屬性被讀取了')
            return val
        },
        set(newVal){
            console.log('price屬性被修改了')
            val = newVal
        }
})

通過Object.defineProperty()方法給car定義了一個price屬性鳖谈,并把這個屬性的讀和寫分別使用get()set()進行攔截岁疼,每當(dāng)該屬性進行讀或?qū)懖僮鞯臅r候就會出發(fā)get()set()。如下圖:

查詢與修改對象

可以看到缆娃,car已經(jīng)可以主動告訴我們它的屬性的讀寫情況了捷绒,這也意味著,這個car的數(shù)據(jù)對象已經(jīng)是“可觀測”的了贯要。

為了把car的所有屬性都變得可觀測暖侨,我們可以編寫如下兩個函數(shù):

    /**
     * 把一個對象的每一項都轉(zhuǎn)化成可觀測對象
     * @param { Object } obj 對象
     */
    function observable (obj) {
        if (!obj || typeof obj !== 'object') {
            return;
        }
        let keys = Object.keys(obj);
        keys.forEach((key) =>{
            defineReactive(obj,key,obj[key])
        })
        return obj;
    }
    /**
     * 使一個對象轉(zhuǎn)化成可觀測對象
     * @param { Object } obj 對象
     * @param { String } key 對象的key
     * @param { Any } val 對象的某個key的值
     */
    function defineReactive (obj,key,val) {
        Object.defineProperty(obj, key, {
            get(){
                console.log(`${key}屬性被讀取了`);
                return val;
            },
            set(newVal){
                console.log(`${key}屬性被修改了`);
                val = newVal;
            }
        })
    }

現(xiàn)在,我們就可以這樣定義car:

let car = observable({
        'brand':'BMW',
        'price':3000
    })

car的兩個屬性都變得可觀測了崇渗。

四字逗、依賴收集

完成了數(shù)據(jù)的'可觀測',即我們知道了數(shù)據(jù)在什么時候被讀或?qū)懥苏悖敲春簦覀兙涂梢栽跀?shù)據(jù)被讀或?qū)懙臅r候通知那些依賴該數(shù)據(jù)的視圖更新了,為了方便乘碑,我們需要先將所有依賴收集起來挖息,一旦數(shù)據(jù)發(fā)生變化,就統(tǒng)一通知更新兽肤。其實套腹,這就是典型的“發(fā)布訂閱者”模式,數(shù)據(jù)變化為“發(fā)布者”资铡,依賴對象為“訂閱者”电禀。

現(xiàn)在,我們需要創(chuàng)建一個依賴收集容器笤休,也就是消息訂閱器Dep尖飞,用來容納所有的“訂閱者”。訂閱器Dep主要負責(zé)收集訂閱者,然后當(dāng)數(shù)據(jù)變化的時候后執(zhí)行對應(yīng)訂閱者的更新函數(shù)政基。

創(chuàng)建消息訂閱器Dep:

class Dep {
        constructor(){
            this.subs = []
        },
        //增加訂閱者
        addSub(sub){
            this.subs.push(sub);
        },
        //判斷是否增加訂閱者
        depend () {
            if (Dep.target) {
                this.addSub(Dep.target)
            }
        },

        //通知訂閱者更新
        notify(){
            this.subs.forEach((sub) =>{
                sub.update()
            })
        }
    }
Dep.target = null;

有了訂閱器贞铣,再將defineReactive函數(shù)進行改造一下,向其植入訂閱器:

function defineReactive (obj,key,val) {
        let dep = new Dep();
        Object.defineProperty(obj, key, {
            get(){
                dep.depend();
                console.log(`${key}屬性被讀取了`);
                return val;
            },
            set(newVal){
                val = newVal;
                console.log(`${key}屬性被修改了`);
                dep.notify()                    //數(shù)據(jù)變化通知所有訂閱者
            }
        })
    }

從代碼上看沮明,我們設(shè)計了一個訂閱器Dep類辕坝,該類里面定義了一些屬性和方法,這里需要特別注意的是它有一個靜態(tài)屬性 target荐健,這是一個全局唯一 的Watcher酱畅,這是一個非常巧妙的設(shè)計,因為在同一時間只能有一個全局的 Watcher 被計算江场,另外它的自身屬性 subs 也是 Watcher 的數(shù)組纺酸。

我們將訂閱器Dep添加訂閱者的操作設(shè)計在getter里面,這是為了讓Watcher初始化時進行觸發(fā)址否,因此需要判斷是否要添加訂閱者餐蔬。在setter函數(shù)里面,如果數(shù)據(jù)變化在张,就會去通知所有訂閱者用含,訂閱者們就會去執(zhí)行對應(yīng)的更新的函數(shù)。

到此帮匾,訂閱器Dep設(shè)計完畢啄骇,接下來,我們設(shè)計訂閱者Watcher.

五瘟斜、訂閱者Watcher

訂閱者Watcher在初始化的時候需要將自己添加進訂閱器Dep中缸夹,那該如何添加呢?我們已經(jīng)知道監(jiān)聽器Observer是在get函數(shù)執(zhí)行了添加訂閱者Wather的操作的螺句,所以我們只要在訂閱者Watcher初始化的時候出發(fā)對應(yīng)的get函數(shù)去執(zhí)行添加訂閱者操作即可虽惭,那要如何觸發(fā)get的函數(shù),再簡單不過了蛇尚,只要獲取對應(yīng)的屬性值就可以觸發(fā)了芽唇,核心原因就是因為我們使用了Object.defineProperty( )進行數(shù)據(jù)監(jiān)聽。這里還有一個細節(jié)點需要處理取劫,我們只要在訂閱者Watcher初始化的時候才需要添加訂閱者匆笤,所以需要做一個判斷操作,因此可以在訂閱器上做一下手腳:在Dep.target上緩存下訂閱者谱邪,添加成功后再將其去掉就可以了炮捧。訂閱者Watcher的實現(xiàn)如下:

class Watcher {
        constructor(vm,exp,cb){
            this.vm = vm;
            this.exp = exp;
            this.cb = cb;
            this.value = this.get();  // 將自己添加到訂閱器的操作
        },

        update(){
            let value = this.vm.data[this.exp];
            let oldVal = this.value;
            if (value !== oldVal) {
                this.value = value;
                this.cb.call(this.vm, value, oldVal);
            },
        get(){
            Dep.target = this;  // 緩存自己
            let value = this.vm.data[this.exp]  // 強制執(zhí)行監(jiān)聽器里的get函數(shù)
            Dep.target = null;  // 釋放自己
            return value;
        }
    }

過程分析:

訂閱者Watcher 是一個 類,在它的構(gòu)造函數(shù)中惦银,定義了一些屬性:

  • vm:一個Vue的實例對象咆课;
  • exp:node節(jié)點的v-modelv-on:click等指令的屬性值末誓。如v-model="name"exp就是name;
  • cb:Watcher綁定的更新函數(shù);

當(dāng)我們?nèi)嵗粋€渲染 watcher 的時候书蚪,首先進入 watcher 的構(gòu)造函數(shù)邏輯喇澡,就會執(zhí)行它的 this.get() 方法,進入 get 函數(shù)善炫,首先會執(zhí)行:

Dep.target = this;  // 緩存自己

實際上就是把 Dep.target 賦值為當(dāng)前的渲染 watcher ,接著又執(zhí)行了:

let value = this.vm.data[this.exp]  // 強制執(zhí)行監(jiān)聽器里的get函數(shù)

在這個過程中會對 vm 上的數(shù)據(jù)訪問撩幽,其實就是為了觸發(fā)數(shù)據(jù)對象的getter库继。

每個對象值的 getter都持有一個 dep箩艺,在觸發(fā) getter 的時候會調(diào)用 dep.depend() 方法,也就會執(zhí)行this.addSub(Dep.target),即把當(dāng)前的 watcher 訂閱到這個數(shù)據(jù)持有的 depsubs 中宪萄,這個目的是為后續(xù)數(shù)據(jù)變化時候能通知到哪些 subs 做準(zhǔn)備艺谆。

這樣實際上已經(jīng)完成了一個依賴收集的過程。那么到這里就結(jié)束了嗎拜英?其實并沒有静汤,完成依賴收集后,還需要把 Dep.target 恢復(fù)成上一個狀態(tài)居凶,即:

Dep.target = null;  // 釋放自己

因為當(dāng)前vm的數(shù)據(jù)依賴收集已經(jīng)完成虫给,那么對應(yīng)的渲染Dep.target 也需要改變。

update()函數(shù)是用來當(dāng)數(shù)據(jù)發(fā)生變化時調(diào)用Watcher自身的更新函數(shù)進行更新的操作侠碧。先通過let value = this.vm.data[this.exp];獲取到最新的數(shù)據(jù),然后將其與之前get()獲得的舊數(shù)據(jù)進行比較抹估,如果不一樣,則調(diào)用更新函數(shù)cb

六弄兜、測試

index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Document</title>
</head>
<body>
    <h1 id="name"></h1>
    <input type="text">
    <input type="button" value="改變data內(nèi)容" onclick="changeInput()">
    
<script src="observer.js"></script>
<script src="watcher.js"></script>
<script>
    function myVue (data, el, exp) {
        this.data = data;
        observable(data);                      //將數(shù)據(jù)變的可觀測
        el.innerHTML = this.data[exp];           // 初始化模板數(shù)據(jù)的值
        new Watcher(this, exp, function (value) {
            el.innerHTML = value;
        });
        return this;
    }

    var ele = document.querySelector('#name');
    var input = document.querySelector('input');
    
    var myVue = new myVue({
        name: 'hello world'
    }, ele, 'name');
    
    //改變輸入框內(nèi)容
    input.oninput = function (e) {
        myVue.data.name = e.target.value
    }
    //改變data內(nèi)容
    function changeInput(){
        myVue.data.name = "難涼熱血"
    
    }
</script>
</body>
</html>

observer.js

/**
     * 把一個對象的每一項都轉(zhuǎn)化成可觀測對象
     * @param { Object } obj 對象
     */
    function observable (obj) {
        if (!obj || typeof obj !== 'object') {
            return;
        }
        let keys = Object.keys(obj);
        keys.forEach((key) =>{
            defineReactive(obj,key,obj[key])
        })
        return obj;
    }
    /**
     * 使一個對象轉(zhuǎn)化成可觀測對象
     * @param { Object } obj 對象
     * @param { String } key 對象的key
     * @param { Any } val 對象的某個key的值
     */
    function defineReactive (obj,key,val) {
        let dep = new Dep();
        Object.defineProperty(obj, key, {
            get(){
                dep.depend();
                console.log(`${key}屬性被讀取了`);
                return val;
            },
            set(newVal){
                val = newVal;
                console.log(`${key}屬性被修改了`);
                dep.notify()                    //數(shù)據(jù)變化通知所有訂閱者
            }
        })
    }
    class Dep {
        
        constructor(){
            this.subs = []
        }
        //增加訂閱者
        addSub(sub){
            this.subs.push(sub);
        }
        //判斷是否增加訂閱者
        depend () {
            if (Dep.target) {
                this.addSub(Dep.target)
            }
        }

        //通知訂閱者更新
        notify(){
            this.subs.forEach((sub) =>{
                sub.update()
            })
        }
        
    }
    Dep.target = null;

watcher.js

class Watcher {
        constructor(vm,exp,cb){
            this.vm = vm;
            this.exp = exp;
            this.cb = cb;
            this.value = this.get();  // 將自己添加到訂閱器的操作
        }
        get(){
            Dep.target = this;  // 緩存自己
            let value = this.vm.data[this.exp]  // 強制執(zhí)行監(jiān)聽器里的get函數(shù)
            Dep.target = null;  // 釋放自己
            return value;
        }
        update(){
            let value = this.vm.data[this.exp];
            let oldVal = this.value;
            if (value !== oldVal) {
                this.value = value;
                this.cb.call(this.vm, value, oldVal);
            }
    }
}

七药蜻、總結(jié)

總結(jié)一下:

實現(xiàn)數(shù)據(jù)的雙向綁定,首先要對數(shù)據(jù)進行劫持監(jiān)聽替饿,所以我們需要設(shè)置一個監(jiān)聽器Observer语泽,用來監(jiān)聽所有屬性。如果屬性發(fā)上變化了视卢,就需要告訴訂閱者Watcher看是否需要更新踱卵。因為訂閱者是有很多個,所以我們需要有一個消息訂閱器Dep來專門收集這些訂閱者据过,然后在監(jiān)聽器Observer和訂閱者Watcher之間進行統(tǒng)一管理的惋砂。

數(shù)據(jù)雙向綁定圖解

本文轉(zhuǎn)載于 此處

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市蝶俱,隨后出現(xiàn)的幾起案子班利,更是在濱河造成了極大的恐慌,老刑警劉巖榨呆,帶你破解...
    沈念sama閱讀 218,607評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件罗标,死亡現(xiàn)場離奇詭異庸队,居然都是意外死亡,警方通過查閱死者的電腦和手機闯割,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,239評論 3 395
  • 文/潘曉璐 我一進店門彻消,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人宙拉,你說我怎么就攤上這事宾尚。” “怎么了谢澈?”我有些...
    開封第一講書人閱讀 164,960評論 0 355
  • 文/不壞的土叔 我叫張陵煌贴,是天一觀的道長。 經(jīng)常有香客問我锥忿,道長牛郑,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,750評論 1 294
  • 正文 為了忘掉前任敬鬓,我火速辦了婚禮淹朋,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘钉答。我一直安慰自己础芍,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,764評論 6 392
  • 文/花漫 我一把揭開白布数尿。 她就那樣靜靜地躺著仑性,像睡著了一般。 火紅的嫁衣襯著肌膚如雪砌创。 梳的紋絲不亂的頭發(fā)上虏缸,一...
    開封第一講書人閱讀 51,604評論 1 305
  • 那天,我揣著相機與錄音嫩实,去河邊找鬼刽辙。 笑死,一個胖子當(dāng)著我的面吹牛甲献,可吹牛的內(nèi)容都是我干的宰缤。 我是一名探鬼主播,決...
    沈念sama閱讀 40,347評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼晃洒,長吁一口氣:“原來是場噩夢啊……” “哼慨灭!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起球及,我...
    開封第一講書人閱讀 39,253評論 0 276
  • 序言:老撾萬榮一對情侶失蹤氧骤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后吃引,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體筹陵,經(jīng)...
    沈念sama閱讀 45,702評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡刽锤,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,893評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了朦佩。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片并思。...
    茶點故事閱讀 40,015評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖语稠,靈堂內(nèi)的尸體忽然破棺而出宋彼,到底是詐尸還是另有隱情,我是刑警寧澤仙畦,帶...
    沈念sama閱讀 35,734評論 5 346
  • 正文 年R本政府宣布输涕,位于F島的核電站,受9級特大地震影響议泵,放射性物質(zhì)發(fā)生泄漏占贫。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,352評論 3 330
  • 文/蒙蒙 一先口、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧瞳收,春花似錦碉京、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,934評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至界弧,卻和暖如春凡蜻,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背垢箕。 一陣腳步聲響...
    開封第一講書人閱讀 33,052評論 1 270
  • 我被黑心中介騙來泰國打工划栓, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人条获。 一個月前我還...
    沈念sama閱讀 48,216評論 3 371
  • 正文 我出身青樓忠荞,卻偏偏與公主長得像,于是被迫代替她去往敵國和親帅掘。 傳聞我的和親對象是個殘疾皇子委煤,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,969評論 2 355

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