如何实现一个MVVM框架

February 24, 2016

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 overview

示意图中可以看出双向数据流:

View将变动通知到ViewModel,然后ViewModel对Model进行更新。
Model将变动通知到ViewModel,然后ViewModel对View进行更新。

其中最核心的功能是对视图(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 overview

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

全面理解React,实现自己的React

通过实现一个简单的React, 来理解React的原理 Continue reading

同构渲染的常见风险

Published on October 01, 2017

React16升级避坑指南

Published on September 10, 2017