MVVM實際是Model - View - ViewModel的模式,這三個組件也是MVVM的核心:vue
Model和View之間使用ViewMode進行關聯,ViewModel負責將Model的數據變化顯示在View上,經過將View的改變反饋到Model上。函數
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進行重寫操做,顧名思義:
因此當執行 Book.name = 'Echo的書📖' 這個語句時,控制檯會打印出 "這本書的名字叫作:Echo的書📖",緊接着,當讀取這個屬性時,就會輸出 "《Echo的書📖'》",由於咱們在get函數裏面對該值作了加工了。
那咱們如今來輸出一下Book,看下結果會是什麼呢?
有沒有發現它的結構和咱們最開始打印的vue初始數據的結構很是類似,這說明vue確實是經過這種方法來進行數據劫持的。
好了,基礎知識咱們已經瞭解完了,那麼問題是,MVVM的雙向綁定到底是如何實現的呢?
實現MVVM主要包括兩個方面:
數據變化更新視圖的重點在於:如何知道數據更新了,其實在咱們以前就已經瞭解過了,就是Object.defineProperty()
。
經過Object.defineProperty()
對屬性設置一個set函數,當數據改變了就會來觸發這個函數,因此咱們只要將一些須要更新的方法放在這裏面就能夠實現data更新view了。
咱們已經知道了,數據的雙向綁定的實現,首先就是須要對數據進行劫持監聽。因此咱們須要設置一個監聽器Observer()
, 用來監聽全部屬性,若是這些屬性中有發生變化的了,就須要告訴訂閱者 Watcher()
是否要更新視圖,由於訂閱者是有不少個的,因此咱們須要有個集中的消息訂閱者 Dep
來管理這些訂閱者。而後在監聽器和訂閱者之間進行統一的管理。
接着,咱們還須要有一個指令解析器Compile,對每一個節點元素進行掃描和解析,將相關指令對應初始化成一個訂閱者Watcher()
,並替換模板數據或者綁定相應的函數,此時當訂閱者Watcher接收到相應屬性的變化,就會執行對應的更新函數,從而更新視圖。
所以接下去咱們執行如下3個步驟,實現數據的雙向綁定:
流程圖以下所示:
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()
稍微改造下,植入消息訂閱器: