參考https://www.pandashen.com/2018/03/28/20180328140039,原文中代碼有一些bug。
MVVM 的前世今生
MVVM 設(shè)計模式,是由 MVC(最早來源于后端)、MVP 等設(shè)計模式進化而來,M - 數(shù)據(jù)模型(Model),VM - 視圖模型(ViewModel),V - 視圖層(View)。
在 MVC 模式中,除了 Model 和 View 層以外,其他所有的邏輯都在 Controller 中,Controller 負(fù)責(zé)顯示頁面、響應(yīng)用戶操作、網(wǎng)絡(luò)請求及與 Model 的交互,隨著業(yè)務(wù)的增加和產(chǎn)品的迭代,Controller 中的處理邏輯越來越多、越來越復(fù)雜,難以維護。為了更好的管理代碼,為了更方便的擴展業(yè)務(wù),必須要為 Controller “瘦身”,需要更清晰的將用戶界面(UI)開發(fā)從應(yīng)用程序的業(yè)務(wù)邏輯與行為中分離,MVVM 為此而生。
很多 MVVM 的實現(xiàn)都是通過數(shù)據(jù)綁定來將 View 的邏輯從其他層分離,可以用下圖來簡略的表示:

使用 MVVM 設(shè)計模式的前端框架很多,其中漸進式框架 Vue 是典型的代表,并在開發(fā)使用中深得廣大前端開發(fā)者的青睞,我們這篇就根據(jù) Vue 對于 MVVM 的實現(xiàn)方式來簡單模擬一版 MVVM 庫。
MVVM 的流程分析
在 Vue 的 MVVM 設(shè)計中,我們主要針對 Compile(模板編譯)、Observer(數(shù)據(jù)劫持)、Watcher(數(shù)據(jù)監(jiān)聽)和 Dep(發(fā)布訂閱)幾個部分來實現(xiàn),核心邏輯流程可參照下圖:

類似這種 “造輪子” 的代碼毋庸置疑一定是通過面向?qū)ο缶幊虂韺崿F(xiàn)的,并嚴(yán)格遵循開放封閉原則,由于 ES5 的面向?qū)ο缶幊瘫容^繁瑣,所以,在接下來的代碼中統(tǒng)一使用 ES6 的 class 來實現(xiàn)。
MVVM 類的實現(xiàn)
在 Vue 中,對外只暴露了一個名為 Vue 的構(gòu)造函數(shù),在使用的時候 new 一個 Vue 實例,然后傳入了一個 options 參數(shù),類型為一個對象,包括當(dāng)前 Vue 實例的作用域 el、模板綁定的數(shù)據(jù) data 等等。
我們模擬這種 MVVM 模式的時候也構(gòu)建一個類,名字就叫 MVVM,在使用時同 Vue 框架類似,需要通過 new 指令創(chuàng)建 MVVM 的實例并傳入 options。
文件:MVVM.js
class MVVM {
constructor(options) {
// 先把 el 和 data 掛在 MVVM 實例上
this.$el = options.el;
this.$data = options.data;
if (this.$el) {
// $data數(shù)據(jù)劫持
new Observer(this.$data);
// 將數(shù)據(jù)代理到實例上 vm.message = "hello"
this.proxyData(this.$data);
// 用數(shù)據(jù)和元素進行編譯
new Compile(this.$el, this);
}
}
proxyData(data) { // 代理數(shù)據(jù)的方法
Object.keys(data).forEach(key => {
Object.defineProperty(this, key, {
get() {
return data[key];
},
set(newVal) {
data[key] = newVal;
}
});
});
}
}
通過上面代碼,我們可以看出,在我們 new 一個 MVVM 的時候,在參數(shù) options 中傳入了一個 Dom 的根元素節(jié)點和數(shù)據(jù) data 并掛在了當(dāng)前的 MVVM 實例上。
當(dāng)存在根節(jié)點的時候,通過 Observer 類對 data 數(shù)據(jù)進行了劫持,并通過 MVVM 實例的方法 proxyData 把 data 中的數(shù)據(jù)掛在當(dāng)前 MVVM 實例上,同樣對數(shù)據(jù)進行了劫持,是因為我們在獲取和修改數(shù)據(jù)的時候可以直接通過 this 或 this.$data,在 Vue 中實現(xiàn)數(shù)據(jù)劫持的核心方法是 Object.defineProperty,我們也使用這個方式通過添加 getter 和 setter 來實現(xiàn)數(shù)據(jù)劫持。
最后使用 Compile 類對模板和綁定的數(shù)據(jù)進行了解析和編譯,并渲染在根節(jié)點上,之所以數(shù)據(jù)劫持和模板解析都使用類的方式實現(xiàn),是因為代碼方便維護和擴展,其實不難看出,MVVM 類其實作為了 Compile 類和 Observer 類的一個橋梁。
模板編譯 Compile 類的實現(xiàn)
Compile 類在創(chuàng)建實例的時候需要傳入兩個參數(shù),第一個參數(shù)是當(dāng)前 MVVM 實例作用的根節(jié)點,第二個參數(shù)就是 MVVM 實例,之所以傳入 MVVM 的實例是為了更方便的獲取 MVVM 實例上的屬性。
在 Compile 類中,我們會盡量的把一些公共的邏輯抽取出來進行最大限度的復(fù)用,避免冗余代碼,提高維護性和擴展性,我們把 Compile 類抽取出的實例方法主要分為兩大類,輔助方法和核心方法,在代碼中用注釋標(biāo)明。
1、解析根節(jié)點內(nèi)的 Dom 結(jié)構(gòu)
文件:Compile.js
class Compile {
constructor(el, vm) {
this.el = this.isElementNode(el) ? el : document.querySelector(el);
this.vm = vm;
// 如過傳入的根元素存在,才開始編譯
if (this.el) {
// 1、把這些真實的 Dom 移動到內(nèi)存中,即 fragment(文檔碎片)
let fragment = this.node2fragment(this.el);
}
}
/* 輔助方法 */
// 判斷是否是元素節(jié)點
isElementNode(node) {
return node.nodeType === 1;
}
/* 核心方法 */
// 將根節(jié)點轉(zhuǎn)移至文檔碎片
node2fragment(el) {
// 創(chuàng)建文檔碎片
let fragment = document.createDocumentFragment();
// 第一個子節(jié)點
let firstChild;
// 循環(huán)取出根節(jié)點中的節(jié)點并放入文檔碎片中
while (firstChild = el.firstChild) {
fragment.appendChild(firstChild);
}
return fragment;
}
}
上面編譯模板的過程中,前提條件是必須存在根元素節(jié)點,傳入的根元素節(jié)點允許是一個真實的 Dom 元素,也可以是一個選擇器,所以我們創(chuàng)建了輔助方法 isElementNode 來幫我們判斷傳入的元素是否是 Dom,如果是就直接使用,是選擇器就獲取這個 Dom,最終將這個根節(jié)點存入 this.el 屬性中。
解析模板的過程中為了性能,我們應(yīng)取出根節(jié)點內(nèi)的子節(jié)點存放在文檔碎片中(內(nèi)存),需要注意的是將一個 Dom 節(jié)點內(nèi)的子節(jié)點存入文檔碎片的過程中,會在原來的 Dom 容器中刪除這個節(jié)點,所以在遍歷根節(jié)點的子節(jié)點時,永遠(yuǎn)是將第一個節(jié)點取出存入文檔碎片,直到節(jié)點不存在為止。
2、編譯文檔碎片中的結(jié)構(gòu)
在 Vue 中的模板編譯的主要就是兩部分,也是瀏覽器無法解析的部分,元素節(jié)點中的指令和文本節(jié)點中的 Mustache 語法(雙大括號)。
文件:Compile.js —— 完善
class Compile {
constructor(el, vm) {
//dom
this.el = this.isElementNode(el) ? el : document.querySelector(el);
this.vm = vm;
// 如過傳入的根元素存在,才開始編譯
if (this.el) {
// 1、把這些真實的 Dom 移動到內(nèi)存中,即 fragment(文檔碎片)
let fragment = this.node2fragment(this.el);
// ********** 以下為新增代碼 **********
// 2、將模板中的指令中的變量和 {{}} 中的變量替換成真實的數(shù)據(jù)
this.compile(fragment);
// 3、把編譯好的 fragment 再塞回頁面中
this.el.appendChild(fragment);
// ********** 以上為新增代碼 **********
}
}
/* 輔助方法 */
// 判斷是否是元素節(jié)點
isElementNode(_node) {
return _node.nodeType === 1;
}
// ********** 以下為新增代碼 **********
// 判斷屬性是否為指令
isDirective(name) {
return name.includes("v-");
}
// ********** 以上為新增代碼 **********
/* 核心方法 */
// 將根節(jié)點轉(zhuǎn)移至文檔碎片
node2fragment(el) {
// 創(chuàng)建文檔碎片
let fragment = document.createDocumentFragment();
// 第一個子節(jié)點
let firstChild;
// 循環(huán)取出根節(jié)點中的節(jié)點并放入文檔碎片中
while (firstChild = el.firstChild) {
fragment.appendChild(firstChild);
}
return fragment;
}
// ********** 以下為新增代碼 **********
// 解析文檔碎片
compile(fragment) {
// 當(dāng)前父節(jié)點節(jié)點的子節(jié)點,包含文本節(jié)點,類數(shù)組對象
let childNodes = fragment.childNodes;
// 轉(zhuǎn)換成數(shù)組并循環(huán)判斷每一個節(jié)點的類型
Array.from(childNodes).forEach(node => {
if (this.isElementNode(node)) { // 是元素節(jié)點
// 遞歸編譯子節(jié)點
this.compile(node);
// 編譯元素節(jié)點的方法
this.compileElement(node);
} else { // 是文本節(jié)點
// 編譯文本節(jié)點的方法
this.compileText(node);
}
});
}
// 編譯元素
compileElement(node) {
// 取出當(dāng)前節(jié)點的屬性,類數(shù)組
let attrs = node.attributes;
Array.from(attrs).forEach(attr => {
// 獲取屬性名,判斷屬性是否為指令,即含 v-
let attrName = attr.name;
if (this.isDirective(attrName)) {
// 如果是指令,取到該屬性值得變量在 data 中對應(yīng)得值,替換到節(jié)點中
let exp = attr.value;
// 取出方法名
let [, type] = attrName.split("-");
// 調(diào)用指令對應(yīng)得方法
CompileUtil[type](node, this.vm, exp);
}
});
}
// 編譯文本
compileText(node) {
// 獲取文本節(jié)點的內(nèi)容
let exp = node.textContent;
// 創(chuàng)建匹配 {{}} 的正則表達(dá)式
//let reg = /\{\{([^}+])\}\}/g;
//“.”表示任意字符?!?”表示前面表達(dá)式一次乃至多次?!?”表示匹配模式是非貪婪的。
let reg = /\{\{(.+?)\}\}/g;
// 如果存在 {{}} 則使用 text 指令的方法
if (reg.test(exp)) {
CompileUtil["text"](node, this.vm, exp);
}
}
// ********** 以上為新增代碼 **********
}
上面代碼新增內(nèi)容得主要邏輯就是做了兩件事:
- 調(diào)用 compile 方法對 fragment 文檔碎片進行編譯,即替換內(nèi)部指令和 Mustache 語法中變量對應(yīng)的值;
- 將編譯好的 fragment 文檔碎片塞回根節(jié)點。
在第一個步驟當(dāng)中邏輯是比較繁瑣的,首先在 compile 方法中獲取所有的子節(jié)點,循環(huán)進行編譯,如果是元素節(jié)點需要遞歸 compile,傳入當(dāng)前元素節(jié)點。在這個過程當(dāng)中抽取出了兩個方法,compileElement 和 compileText 用來對元素節(jié)點的屬性和文本節(jié)點進行處理。
compileElement 中的核心邏輯就是處理指令,取出元素節(jié)點所有的屬性判斷是否是指令,是指令則調(diào)用指令對應(yīng)的方法。compileText 中的核心邏輯就是取出文本的內(nèi)容通過正則表達(dá)式匹配出被 Mustache 語法的 “{{ }}” 包裹的內(nèi)容,并調(diào)用處理文本的 text 方法。
文本節(jié)點的內(nèi)容有可能存在 “{{ }} {{ }} {{ }}”,正則匹配默認(rèn)是貪婪的,為了防止第一個 “{” 和最后一個 “}” 進行匹配,所以在正則表達(dá)式中應(yīng)使用非貪婪匹配。
在調(diào)用指令的方法時都是調(diào)用的 CompileUtil 下對應(yīng)的方法,我們之所以單獨把這些指令對應(yīng)的方法抽離出來存儲在 CompileUtil 對象下的目的是為了解耦,因為后面其他的類還要使用。
3、CompileUtil 對象中指令方法的實現(xiàn)
CompileUtil 中存儲著所有的指令方法及指令對應(yīng)的更新方法,由于 Vue 的指令很多,我們這里只實現(xiàn)比較典型的 v-model 和 “{{ }}” 對應(yīng)的方法,考慮到后續(xù)更新的情況,我們統(tǒng)一把設(shè)置值到 Dom 中的邏輯抽取出對應(yīng)上面兩種情況的方法,存放到 CompileUtil 的 updater 對象中。
文件:CompileUtil.js
CompileUtil = {};
// 更新Dom節(jié)點方法
CompileUtil.updater = {
// 文本更新
textUpdater(node, value) {
node.textContent = value;
},
// 輸入框更新
modelUpdater(node, value) {
node.value = value;
}
};
// 獲取 data 值的方法
CompileUtil.getVal = function (vm, exp) {
// 將匹配的值用 . 分割開,如 vm.data.a.b
exp = exp.split(".");
// 歸并取值
return exp.reduce((prev, next) => {
return prev[next];
}, vm.$data);
};
// 獲取文本 {{}} 中變量在 data 對應(yīng)的值
CompileUtil.getTextVal = function (vm, exp) {
// 使用正則匹配出 {{ }} 間的變量名,再調(diào)用 getVal 獲取值
return exp.replace(/\{\{([^}]+)\}\}/g, (...args) => {
return this.getVal(vm, args[1]);
});
};
// 設(shè)置 data 值的方法
CompileUtil.setVal = function (vm, exp, newVal) {
exp = exp.split(".");
return exp.reduce((prev, next, currentIndex) => {
// 如果當(dāng)前歸并的為數(shù)組的最后一項,則將新值設(shè)置到該屬性
if(currentIndex === exp.length - 1) {
return prev[next] = newVal
}
// 繼續(xù)歸并
return prev[next];
}, vm.$data);
}
// 處理 v-model 指令的方法
CompileUtil.model = function (node, vm, exp) {
// 獲取賦值的方法
let updateFn = this.updater["modelUpdater"];
// 獲取 data 中對應(yīng)的變量的值
let value = this.getVal(vm, exp);
// 添加觀察者,作用與 text 方法相同
new Watcher(vm, exp, newValue => {
updateFn && updateFn(node, newValue);
});
// v-model 雙向數(shù)據(jù)綁定,對 input 添加事件監(jiān)聽
node.addEventListener('input', e => {
// 獲取輸入的新值
let newValue = e.target.value;
// 更新到節(jié)點
this.setVal(vm, exp, newValue);
});
// 第一次設(shè)置值
updateFn && updateFn(node, value);
};
// 處理文本節(jié)點 {{}} 的方法
CompileUtil.text = function (node, vm, exp) {
// 獲取賦值的方法
let updateFn = this.updater["textUpdater"];
// 獲取 data 中對應(yīng)的變量的值
let value = this.getTextVal(vm, exp);
// 通過正則替換,將取到數(shù)據(jù)中的值替換掉 {{ }}
exp.replace(/\{\{(.+?)\}\}/g, (...args) => {
// 解析時遇到了模板中需要替換為數(shù)據(jù)值的變量時,應(yīng)該添加一個觀察者
// 當(dāng)變量重新賦值時,調(diào)用更新值節(jié)點到 Dom 的方法
new Watcher(vm, args[1], newValue => {
// 如果數(shù)據(jù)發(fā)生變化,重新獲取新值
updateFn && updateFn(node, newValue);
});
});
// 第一次設(shè)置值
updateFn && updateFn(node, value);
};
這部分的整個思路就是在 Compile 編譯模板后處理 v-model 和 “{{ }}” 時,其實都是用 data 中的數(shù)據(jù)替換掉 fragment 文檔碎片中對應(yīng)的節(jié)點中的變量。因此會經(jīng)常性的獲取 data 中的值,在更新節(jié)點時又會重新設(shè)置 data 中的值,所以我們抽離出了三個方法 getVal、getTextVal 和 setVal 掛在了 CompileUtil 對象下。
獲取和設(shè)置 data 的值兩個方法 getVal 和 setVal 思路相似,由于獲取的變量層級不定,可能是 data.a,也可能是 data.obj.a.b,所以都是使用歸并的思路,借用 reduce 方法實現(xiàn)的,區(qū)別在于 setVal 方法在歸并過程中需要判斷是不是歸并到最后一級,如果是則設(shè)置新值,而 getTextVal 就是在 getVal 外包了一層處理 “{{ }}” 的邏輯。
在這些準(zhǔn)備工作就緒以后就可以實現(xiàn)我們的主邏輯,即對 Compile 類中解析的文本節(jié)點和元素節(jié)點指令中的變量用 data 值進行替換,還記得前面說針對 v-model 和 “{{ }}” 進行處理,因此設(shè)計了 model 和 text 兩個核心方法。
model和text兩個方法邏輯相似,都獲取了各自的 updater 中的方法,對值進行設(shè)置,并且在設(shè)置的同時為了后續(xù) data 中的數(shù)據(jù)修改,視圖的更新,創(chuàng)建了 Watcher 的實例,并在內(nèi)部用新值重新更新節(jié)點,不同的是 Vue 的 v-model 指令在表單中實現(xiàn)了雙向數(shù)據(jù)綁定,只要表單元素的 value 值發(fā)生變化,就需要將新值更新到 data 中,并響應(yīng)到頁面上。
所以我們的實現(xiàn)方式是給這個綁定了 v-model 的表單元素監(jiān)聽了 input 事件,并在事件中實時的將新的 value 值更新到 data 中,至于 data 中的改變后響應(yīng)到頁面中需要另外三個類 Watcher、Observer 和 Dep 共同實現(xiàn),我們下面就來實現(xiàn) Watcher 類。
觀察者 Watcher 類的實現(xiàn)
在 CompileUtil 對象的方法中創(chuàng)建 Watcher 實例的時候傳入了三個參數(shù),即 MVVM 的實例、模板綁定數(shù)據(jù)的變量名 exp 和一個 callback,這個 callback 內(nèi)部邏輯是為了更新數(shù)據(jù)到 Dom,所以我們的 Watcher 類內(nèi)部要做的事情就清晰了,獲取更改前的值存儲起來,并創(chuàng)建一個 update 實例方法,在值被更改時去執(zhí)行實例的 callback 以達(dá)到視圖的更新。
文件:Watcher.js
class Watcher {
constructor(vm, exp, callback) {
this.vm = vm;
this.exp = exp;
this.callback = callback;
// 更改前的值
this.value = this.get();
}
get() {
// 將當(dāng)前的 watcher 添加到 Dep 類的靜態(tài)屬性上
Dep.target = this;
// 獲取值觸發(fā)數(shù)據(jù)劫持
let value = CompileUtil.getVal(this.vm, this.exp);
// 清空 Dep 上的 Watcher,防止重復(fù)添加
Dep.target = null;
return value;
}
update() {
// 獲取新值
let newValue = CompileUtil.getVal(this.vm, this.exp);
// 獲取舊值
let oldValue = this.value;
// 如果新值和舊值不相等,就執(zhí)行 callback 對 dom 進行更新
if(newValue !== oldValue) {
this.callback(newValue);
}
}
}
看到上面代碼一定有兩個疑問:
- 使用 get 方法獲取舊值得時候為什么要將當(dāng)前的實例掛在 Dep 上,在獲取值后為什么又清空了;
- update 方法內(nèi)部執(zhí)行了 callback 函數(shù),但是 update 在什么時候執(zhí)行。
這就是后面兩個類 Dep 和 observer 要做的事情,我們首先來介紹 Dep,再介紹 Observer 最后把他們之間的關(guān)系整個串聯(lián)起來。
發(fā)布訂閱 Dep 類的實現(xiàn)
其實發(fā)布訂閱說白了就是把要執(zhí)行的函數(shù)統(tǒng)一存儲在一個數(shù)組中管理,當(dāng)達(dá)到某個執(zhí)行條件時,循環(huán)這個數(shù)組并執(zhí)行每一個成員。
文件:Dep.js
class Dep {
constructor() {
this.subs = [];
}
// 添加訂閱
addSub(watcher) {
this.subs.push(watcher);
}
// 通知
notify() {
this.subs.forEach(watcher => watcher.update());
}
}
在 Dep 類中只有一個屬性,就是一個名為 subs 的數(shù)組,用來管理每一個 watcher,即 Watcher 類的實例,而 addSub 就是用來將 watcher 添加到 subs 數(shù)組中的,我們看到 notify 方法就解決了上面的一個疑問,Watcher 類的 update 方法是怎么執(zhí)行的,就是這樣循環(huán)執(zhí)行的。
接下來我們整合一下盲點:
- Dep 實例在哪里創(chuàng)建聲明,又是在哪里將 watcher 添加進 subs 數(shù)組的;
- Dep 的 notify 方法應(yīng)該在哪里調(diào)用;
- Watcher 內(nèi)容中,使用 get 方法獲取舊值得時候為什么要將當(dāng)前的實例掛在 Dep 上,在獲取值后為什么又清空了。
這些問題在最后一個類 Observer 實現(xiàn)的時候都將清晰,下面我們重點來看最后一部分核心邏輯。
數(shù)據(jù)劫持 Observer 類的實現(xiàn)
還記得實現(xiàn) MVVM 類的時候就創(chuàng)建了這個類的實例,當(dāng)時傳入的參數(shù)是 MVVM 實例的 data 屬性,在 MVVM 中把數(shù)據(jù)通過 Object.defineProperty 掛到了實例上,并添加了 getter 和 setter,其實 Observer 類主要目的就是給 data 內(nèi)的所有層級的數(shù)據(jù)都進行這樣的操作。
文件:Observer.js
class Observer {
constructor (data) {
this.observe(data);
}
// 添加數(shù)據(jù)監(jiān)聽
observe(data) {
if(!data || typeof data !== 'object') {
return;
}
Object.keys(data).forEach(key => {
// 劫持(實現(xiàn)數(shù)據(jù)響應(yīng)式)
this.defineReactive(data, key, data[key]);
this.observe(data[key]); // 深度劫持
});
}
// 數(shù)據(jù)響應(yīng)式
defineReactive (object, key, value) {
let _this = this;
// 每個變化的數(shù)據(jù)都會對應(yīng)一個數(shù)組,這個數(shù)組是存放所有更新的操作
let dep = new Dep();
// 獲取某個值被監(jiān)聽到
Object.defineProperty(object, key, {
enumerable: true,
configurable: true,
get () { // 當(dāng)取值時調(diào)用的方法
Dep.target && dep.addSub(Dep.target);
return value;
},
set (newValue) { // 當(dāng)給 data 屬性中設(shè)置的值適合,更改獲取的屬性的值
if(newValue !== value) {
_this.observe(newValue); // 重新賦值如果是對象進行深度劫持
value = newValue;
dep.notify(); // 通知所有人數(shù)據(jù)更新了
}
}
});
}
}
在的代碼中 observe 的目的是遍歷對象,在內(nèi)部對數(shù)據(jù)進行劫持,即添加 getter 和 setter,我們把劫持的邏輯單獨抽取成 defineReactive 方法,需要注意的是 observe 方法在執(zhí)行最初就對當(dāng)前的數(shù)據(jù)進行了數(shù)據(jù)類型驗證,然后再循環(huán)對象每一個屬性進行劫持,目的是給同為 Object 類型的子屬性遞歸調(diào)用 observe 進行深度劫持。
在 defineReactive 方法中,創(chuàng)建了 Dep 的實例,并對 data 的數(shù)據(jù)使用 get 和 set 進行劫持,還記得在模板編譯的過程中,遇到模板中綁定的變量,就會解析,并創(chuàng)建 watcher,會在 Watcher 類的內(nèi)部獲取舊值,即當(dāng)前的值,這樣就觸發(fā)了 get,在 get 中就可以將這個 watcher 添加到 Dep 的 subs 數(shù)組中進行統(tǒng)一管理,因為在代碼中獲取 data 中的值操作比較多,會經(jīng)常觸發(fā) get,我們又要保證 watcher 不會被重復(fù)添加,所以在 Watcher 類中,獲取舊值并保存后,立即將 Dep.target 賦值為 null,并且在觸發(fā) get 時對 Dep.target 進行了短路操作,存在才調(diào)用 Dep 的 addSub 進行添加。
而 data 中的值被更改時,會觸發(fā) set,在 set 中做了性能優(yōu)化,即判斷重新賦的值與舊值是否相等,如果相等就不重新渲染頁面,不等的情況有兩種,如果原來這個被改變的值是基本數(shù)據(jù)類型沒什么影響,如果是引用類型,我們需要對這個引用類型內(nèi)部的數(shù)據(jù)進行劫持,因此遞歸調(diào)用了 observe,最后調(diào)用 Dep 的 notify 方法進行通知,執(zhí)行 notify 就會執(zhí)行 subs 中所有被管理的 watcher 的 update,就會執(zhí)行創(chuàng)建 watcher 時的傳入的 callback,就會更新頁面。
在 MVVM 類將 data 的屬性掛在 MVVM 實例上并劫持與通過 Observer 類對 data 的劫持還有一層聯(lián)系,因為整個發(fā)布訂閱的邏輯都是在 data 的 get 和 set 上,只要觸發(fā)了 MVVM 中的 get 和 set 內(nèi)部會自動返回或設(shè)置 data 對應(yīng)的值,就會觸發(fā) data 的 get 和 set,就會執(zhí)行發(fā)布訂閱的邏輯。
通過上面長篇大論的敘述后,這個 MVVM 模式用到的幾個類的關(guān)系應(yīng)該完全敘述清晰了,雖然比較抽象,但是細(xì)心琢磨還是會明白之間的關(guān)系和邏輯,下面我們就來對我們自己實現(xiàn)的這個 MVVM 進行驗證。
驗證 MVVM
我們按照 Vue 的方式根據(jù)自己的 MVVM 實現(xiàn)的內(nèi)容簡單的寫了一個模板如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>MVVM</title>
</head>
<body>
<div id="app">
<!-- 雙向數(shù)據(jù)綁定 靠的是表單 -->
<input type="text" v-model="message">
<div>{{message}}</div>
<ul>
<li>{{message}}</li>
</ul>
{{message}}
</div>
<!-- 引入依賴的 js 文件 -->
<script src="./js/Watcher.js"></script>
<script src="./js/Observer.js"></script>
<script src="./js/Compile.js"></script>
<script src="./js/CompileUtil.js"></script>
<script src="./js/Dep.js"></script>
<script src="./js/MVVM.js"></script>
<script>
let vm = new MVVM({
el: '#app',
data: {
message: 'hello world!'
}
});
</script>
</body>
</html>
打開 Chrom 瀏覽器的控制臺,在上面通過下面操作來驗證:
- 輸入 vm.message = "hello" 看頁面是否更新;
- 輸入 vm.$data.message = "hello" 看頁面是否更新;
改變文本輸入框內(nèi)的值,看頁面的其他元素是否更新。
總結(jié)
通過上面的測試,相信應(yīng)該理解了 MVVM 模式對于前端開發(fā)重大的意義,實現(xiàn)了雙向數(shù)據(jù)綁定,實時保證 View 層與 Model 層的數(shù)據(jù)同步,并可以讓我們在開發(fā)時基于數(shù)據(jù)編程,而最少的操作 Dom,這樣大大提高了頁面渲染的性能,也可以使我們把更多的精力用于業(yè)務(wù)邏輯的開發(fā)上。