Vue雙向綁定的實現

一. MVVM

MVVM實際是Model - View - ViewModel的模式,這三個組件也是MVVM的核心:vue

  • Model —— 包含了業務和驗證邏輯的數據模型
  • View —— 定義屏幕中View的結構,佈局和外觀
  • ViewModel —— 扮演「View」和「Model」之間的使者,幫忙處理 View 的所有業務邏輯

Model和View之間使用ViewMode進行關聯,ViewModel負責將Model的數據變化顯示在View上,經過將View的改變反饋到Model上。函數

二. Object.defineProperty()

Vue的數據雙向綁定是經過數據劫持結合發佈者-訂閱者模式的方式來實現的,咱們能夠先來看一下,若是在控制檯輸出一個定義在vue初始數據上的對象是個什麼東西:佈局

let vm = new Vue({
    data: {
        obj: {
            a: 1
        }
    },
    created: function () {
        console.log(this.obj);
    }
});
複製代碼

結果以下:ui

咱們能夠看到屬性a有兩個相對應的get() / set()方法,爲何會多出這兩個方法呢?由於Vue是經過 Object.defineProperty()來實現數據劫持的。 那麼Object.defineProperty()是用來幹什麼的呢?this

能夠從這裏來看看MDN的官方定義spa

它說: Object.defineProperty() 方法會直接在一個對象上定義一個新屬性,或者修改一個對象的現有屬性,並返回此對象。雙向綁定

意思是Object.defineProperty()能夠來控制一個對象屬性的一些特有操做,好比讀寫權、是否能夠枚舉等等,具體查看文檔。code

而這裏咱們研究下它的get() / set()屬性。cdn

按照咱們以往的方式,咱們獲取一個對象的屬性能夠這樣:server

var Book = {
    name: 'Echo的書'
};
console.log(Book.name); // 輸出'Echo的書'
複製代碼

那麼若是如今,咱們想要在輸出的結果中,自動給書加上書名號應該怎麼作呢?這時就要用到Object.defineProperty()get() / set()方法了:

var Book = {};
var name = '';

Object.defineProperty(Book, 'name', {
    set: function(value){
        name = value;
        console.log('這本書的名字叫作:' + name);
    },
    get: function(value){
        return '《' + name + '》';
    }
})

Book.name = 'Echo的書📖';
console.log(Book.name); 
複製代碼

結果以下:

咱們經過Object.defineProperty()設置了對象Book的name屬性,對其get和set進行重寫操做,顧名思義:

  • get就是在讀取name屬性這個值觸發的函數
  • set就是在設置name屬性這個值觸發的函數

因此當執行 Book.name = 'Echo的書📖' 這個語句時,控制檯會打印出 "這本書的名字叫作:Echo的書📖",緊接着,當讀取這個屬性時,就會輸出 "《Echo的書📖'》",由於咱們在get函數裏面對該值作了加工了。

那咱們如今來輸出一下Book,看下結果會是什麼呢?

有沒有發現它的結構和咱們最開始打印的vue初始數據的結構很是類似,這說明vue確實是經過這種方法來進行數據劫持的。

好了,基礎知識咱們已經瞭解完了,那麼問題是,MVVM的雙向綁定到底是如何實現的呢?

三. 雙向綁定的實現

實現MVVM主要包括兩個方面:

  • 視圖變化更新數據
  • 數據變化更新視圖

第一點:視圖變化更新數據 比較簡單就能實現,經過事件監聽就能實現:好比input中監聽它的input事件,獲取到更新的數據,再傳遞給實際數據就能實現了。因此咱們如今主要來分析:當數據更新變化時如何更新視圖。

數據變化更新視圖的重點在於:如何知道數據更新了,其實在咱們以前就已經瞭解過了,就是Object.defineProperty()

經過Object.defineProperty()對屬性設置一個set函數,當數據改變了就會來觸發這個函數,因此咱們只要將一些須要更新的方法放在這裏面就能夠實現data更新view了。

四. 實現過程

咱們已經知道了,數據的雙向綁定的實現,首先就是須要對數據進行劫持監聽。因此咱們須要設置一個監聽器Observer(), 用來監聽全部屬性,若是這些屬性中有發生變化的了,就須要告訴訂閱者 Watcher() 是否要更新視圖,由於訂閱者是有不少個的,因此咱們須要有個集中的消息訂閱者 Dep 來管理這些訂閱者。而後在監聽器和訂閱者之間進行統一的管理。

接着,咱們還須要有一個指令解析器Compile,對每一個節點元素進行掃描和解析,將相關指令對應初始化成一個訂閱者Watcher(),並替換模板數據或者綁定相應的函數,此時當訂閱者Watcher接收到相應屬性的變化,就會執行對應的更新函數,從而更新視圖。

所以接下去咱們執行如下3個步驟,實現數據的雙向綁定:

  1. 實現一個監聽器Observer,用來劫持並監聽全部屬性,若是有變更的,就通知訂閱者。
  2. 實現一個訂閱者Watcher,能夠收到屬性的變化通知並執行相應的函數,從而更新視圖。
  3. 實現一個解析器Compile,能夠掃描和解析每一個節點的相關指令,並根據初始化模板數據以及初始化相應的訂閱器。

流程圖以下所示:

1. 實現一個監聽器Observer()

Observer()是一個數據監聽器,其核心的方法就是咱們上面所說的 Object.defineProperty(), 若是要對全部的屬性都進行監聽的話,那麼能夠經過遞歸的方法遍歷全部屬性,並對其進行Object.defineProperty()處理,具體實現以下所示:

// 遍歷函數
function observer(datas){
    if(!datas || typeof datas !=='object'){
        return;
    }
    Object.keys(datas).forEach((key)=>{
        defineReactive(datas, key, datas[key]);
    });
}
複製代碼
function defineReactive(datas, key, val){
    observer(val);
    
    Object.defineProperty(datas, key, {
        enumerable: true,
        configurable: true,
        set(newVal){
            val = newVal;
            console.log(`屬性${key}被監聽了,它的新值爲${newVal.toString()}`);
        },
        get(){
            return val;
        }
    });
}

let datas = {
    book: {
        name: 'Echo的書',
        author: 'Echo',
        buyInfos: {
            money: '¥99'
        }
    },
    pushlishTime: '2020-01-01'
};
observer(datas);
datas.book.name = 'Echo新買的書';
datas.book.buyInfos.money = '¥109';
複製代碼

這樣就實現了一個簡單的監聽器啦~

接下來,咱們按照思路,須要建立一個能夠容納訂閱者的消息訂閱器Dep,訂閱器Dep主要負責收集訂閱者Watcher(),而後再屬性變化的時候執行對應訂閱者的更新函數。因此顯然訂閱器須要有一個容器,這個容器就是list,將上面的Observer()稍微改造下,植入消息訂閱器:

相關文章
相關標籤/搜索