如何實現一個MVVM框架

Lea81A 8年前發布 | 45K 次閱讀 MVVM模式 .NET開發

來自: 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等。最終效果如下:

JS Bin on jsbin.com

詳細源代碼在請到github下載

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>

 本文由用戶 Lea81A 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!