如何實現一個MVVM框架
來自: http://foio.github.io/mvvm-overview/
MVVM(Model View ViewModel)最初由微軟在Windows Presentation Foundation(WPF)和Silverlight中引入,近年來、它作為MVC的一種替代方案在前端也如日中天。像其他MV*一樣,MVVM中的Model代表著我們應用的數據;而View代表著用戶界面;最重要的是ViewModel,可以將其看作一個擁有雙向數據處理能力的轉換器,它將模型數據傳遞到視圖,并將視圖指令傳遞到模型。MVVM框架將前端工程師從繁瑣的DOM操作中徹底地解放出來,讓我們可以更專注于自己的業務。
接下來我們探討一種實現雙向綁定的方案,本文適合實際使用過MVVM框架的人閱讀,包括AngularJS、Avalon等。最終效果如下:
1.基本功能
雙向綁定作為MVVM框架的最大特點,是如何實現的呢?MVVM數據流示意圖如下:
示意圖中可以看出雙向數據流:
View將變動通知到ViewModel,然后ViewModel對Model進行更新。
Model將變動通知到ViewModel,然后ViewModel對View進行更新。
其中最核心的功能是對視圖(View)和模型(Model)變動的監聽。
(1).視圖變動的監聽
MVVM框架都是通過相應的指令,在HTML中聲明式的標記出需要監聽的DOM節點。本文實現中,我們主要涉及到兩個指令: foio-controller 、 foio-model 以及一個表達式{{}}。 比如:
<input type="text" foio-model="nickname">
上述指令foio-model,聲明將View中的input的變動通知到Model中的nickname。通過對的視圖節點(input)注冊監聽函數就可以得到視圖(input)的變動了。
//對視圖中的input節點注冊input事件監聽函數
var elem = document.querySelector('input');
if (elem.addEventListener) {
elem.addEventListener('input', callback, false);
} else {
elem.attachEvent('oninput', callback);
}
(2).模型變動的監聽
對模型變動的監聽可以通過ECMAScript5中的API實現。
Object.defineProperty(obj, prop, descriptor)
可以通過該API為對象添加一個屬性,并設置該屬性的gett函數和set函數,在訪問屬性時會觸發相應的get函數和set函數。
var air = {};
Object.defineProperty(air, 'temperature', {
get: function() {
console.log('get!');
},
set: function(value) {
console.log('set!');
}
});
air.temperature = 15; //output: set!
air.temperature; //outpu: get!</code></pre>
我們可以在set函數中得到模型的變動,并將相關變動通知到ViewModel。
2.總體實現
MVVM的主要流程包括(View)視圖掃描、(Model)模型構建、以及關聯視圖和模型(ViewModel)
(1)View(視圖)掃描
處理View(視圖)必然涉及到對DOM結構的掃描,通過掃描抽取指令(本文只有三種指令,foio-controller、foio-model、);并對相應的節點進行如下處理:
綁定通知函數,用于在視圖更新時通知ViewModel
綁定更新函數,用于在模型更新時通過該函數更新視圖
針對不同的節點類型,這些通知函數和更新函數都是預先定義好的,存儲在 directives 結構中。在節點掃描過程中,當遇到指令時,就通過executeBindings函數對相應的節點進行綁定處理。流程圖如下:
(2)Model(模型)構建
而對Model的處理也主要是注冊監聽函數,用于在Model變化時得到通知,如上圖所示。controller中的每一個變量都通過 Object.defineProperty(obj, prop, descriptor) 定義到Model上,其中descriptor上的get函數可以用于搜集依賴,而set函數則用于通知依賴于該Model的視圖進行更新。
var descriptor = {
var dependencyList = [];
get: function() {
//搜集依賴
dependencyList.push(this);
return value;
},
set: function(newVal) {
if (oldVal === newVal) {
return;
}
oldVal = newVal;
//通知依賴于該Model的視圖進行更新
for (var dependIdx in dependencyList) {
dependencyList[dependIdx].updateView(newVal);
}
}
}
(3)關聯模型和視圖
View(視圖)掃描的結果是一個元素集合
bindings = [
{
type: type, //指令類型
element: elem, //DOM節點
expr: value, //綁定的變量名稱
},
{...}
]
而Model(模型)構建的結果也是一個集合:
vmodels = {
controller1: {
expr1: value1,
expr2: value2,
binder: {expr1: function(){},expr2:function(){}}
},
controller1: {...}
}
通過executeBindings函數,將視圖和模型關聯起來。
function executeBindings(bindings, vmodels) {
for (var i = 0, binding; (binding = bindings[i++]);) {
binding.vmodels = vmodels;
directives[binding.type](binding);
};
}
每一種指令都有不同的初始化函數,比如針對 foio-model 指令,當DOM節點為input類型時,初始化函數做了三件事:
監聽input和DOMAutoComplete事件
注冊對模型的依賴
提供更新該DOM節點的方法
詳細代碼如下:
directives['model']={
switch (binding.xtype) {
case "input":
//綁定input事件
binding.bound('input', updateVModel);
//綁定DOMAutoComplete事件
binding.bound('DOMAutoComplete', updateVModel);
//注冊對模型的依賴
elem.value = closetVmodel.binder[binding.expr].apply(binding);
//更新該DOM節點的方法
binding.updateView = function(newVal) {
elem.value = newVal;
};
break;
}
}
至此我們實現了一個基本的MVVM框架了,雖然只有三個指令,但是基本能夠說明如何設計并實現一個MVVM框架了。
</code></code></code></code></code></code></code></code></code></code></code></div>