如何實現一個MVVM框架

積木村の研究所發表於2016-02-24

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資料流示意圖如下:

mvvm

示意圖中可以看出雙向資料流:

View將變動通知到ViewModel,然後ViewModel對Model進行更新。
其中最核心的功能是對檢視(View)和模型(Model)變動的監聽。

(1).檢視變動的監聽

MVVM框架都是通過相應的指令,在HTML中宣告式的標記出需要監聽的DOM節點。本文實現中,我們主要涉及到兩個指令:foio-controllerfoio-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!

我們可以在set函式中得到模型的變動,並將相關變動通知到ViewModel。

2.總體實現

MVVM的主要流程包括(View)檢視掃描、(Model)模型構建、以及關聯檢視和模型(ViewModel)

(1)View(檢視)掃描

處理View(檢視)必然涉及到對DOM結構的掃描,通過掃描抽取指令(本文只有三種指令,foio-controller、foio-model、{{}});並對相應的節點進行如下處理:

繫結通知函式,用於在檢視更新時通知ViewModel
繫結更新函式,用於在模型更新時通過該函式更新檢視

針對不同的節點型別,這些通知函式和更新函式都是預先定義好的,儲存在directives結構中。在節點掃描過程中,當遇到指令時,就通過executeBindings函式對相應的節點進行繫結處理。流程圖如下:

mvvm-flow

(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框架了。

本文同時發表在我的部落格積木村の研究所http://foio.github.io/mvvm-overview/

相關文章