Ruby's Louvre

每天学习一点点算法

导航

我的MVVM框架 v3发布!

人们总是爱探求完美的东西,医学界追求长生不死之药,炼金术师追求贤者之石,物理学家追求永动机……编程界也有自己的追求,完美架构什么的,什么从MVC,到MVP,到MVVM……当然MVC,MVP,MVVM有他们不同的场景,但MVVM在微软试水后已被证实为界面开发最好的方案了。于前端而言,一直纠缠于模板与组件的优劣。其实答案大家都知道,web page用模板, web app用组件,问题是如何将它们统合起来。页面之所以能交互,是因为它存在状态。因此核心问题是如何将这两者管辖的状态统合在一起。状态的由来有两个,直接从模型得到,比如后端传送过来的JSON与XML,或比较悲剧地把PHP序列化的字段还原出来,第二种是在程序中控制流程时不生的中间量。我们可以这些后端数据或中间量整成一个数据源,或作为模板的填充数据,或作为组件的传参最后成为它的属性。MVVM的出现就很好解决这问题,数据是数据,最好作为贫血模型而存在,对这些数据的操作以及基于这些操作的操作独立成另一个东西,ViewModel。View是设计师与页面重构师的阵地。于是我们能实现并行开发。View与ViewModel的连结是数据绑定,ko称之为声明式绑定。绑定可以分解成绑定器与相应的参数,它们调用ViewModel的东西,ViewModel操作Model。绑定器通常是非常薄的一层,很少被人提起,但意义重大。它实现了数据再加工与事件绑定(WPF称之为命令),从而让数据可视,操作可用!整个流程是非常清晰的,得到Model,抽象出ViewModel,在View中声明绑定,就完了。觉得框架提供的功能不足,就添加自定绑定器,在ViewModel添加命令(它可以作为事件回调,或数据的过滤器,验证函数,格式函数)。格式是固定的,像后端那梓,哪个页面应该由哪个action负责都有规可循。这正是我们前端梦寐以来的究极解决方案。

但思路是有,实现起来不轻松,因此前端的MVVM框架也林林总总,每个月都冒出一两个出来。开源的世界类似于免费的世界,很容易引起马太效应,强者愈强,弱者愈弱,很易产生垄断。jQuery的一枝独秀也证明了这一点。MVVM框架的混乱说明还没有出现足够称得上强大的东西。现在比较拉风的是knouckoutjs,emberjs,angularjs,backbone(它也有数据绑定插件让它改装成真正的MVVM)。个人是比较看好knouckoutjs,毕竟是由MVVM的发源地微软的人搞的,是最正统的派系。但它的绑定也一直被人诟病,太复杂难用。后端的WPF由微软的强大工具撑着,因此人们觉得不怎么。但一旦要你们手写这些绑定时就惨了,加之前端经过jQuery那极简主义的DSL式API洗礼后,很多人无法接受这样复杂的用法。emberjs与angularjs对IE6支持不佳,因此在大陆没有销路,加之提供的API太多了,对应的概念也多,学习曲线陡峭。backbone是太笨重,没有干什么活,却要写一大堆代码,与jQuery反向而行。

我的MVVM框架avalon v3两个重要借鉴者为knouckoutjs与rivetsjs。从knouckoutjs得到它的双向依赖链的架构,从学习到消化经历两个版本,v2的实现完全原创。从rivetsjs得到它的声明式绑定的API设计,但实现完全是自己的。v3对v2的双向依赖链的架构进行一些改进,只要是重命名,让这些概念更让人接受。

avalon v3的双向绑定链架构图

    // ViewModel              框架              View
    //属性访问器  ┓
    //组合访问器 ┫→→→绑定器 ←←← DOM访问器 ←←← 数据绑定
    //集合访问器 ┫
    //命令       ┛

ViewModel是一个由访问器与命令组成的对象。访问器即accessor, 取义自ruby的attr_accessor,是attr_writer, attr_reader的结合,用于对某个数据进行读写操作。比如Model中有个aaa属性,ViewModel就会对应生成一个叫aaa的函数,我们可以传参修改这个aaa的值,也可以从它那时得到aaa的值。之所以这么大周折,是因为IE9才支持用Object.defineProperty描述对象的属性的访问机制,它是否可遍历啊,可配置啊,读取时应该返回什么,写入时会进行什么处理。如果aaa属性与bbb属性有关联,我们可以在访问aaa时修改bbb,直接obj.aaa = "xxx"就行了。但为了兼并IE6,我们唯有obj.aaa("xxx")。emberjs就是基于Object.defineProperty构建它的双向绑定链,因此对IE9-支持不好。


//IE9+ FF4+, safari5+, opera11+, chrome5(IE8只支持DOM)
var obj = {}, aValue = 0;
Object.defineProperty(obj, "aaa", {
    get : function(){
        return aValue;
    },
    set : function(newValue){ 
        obj.bbb += newValue
        aValue = newValue; 
    },
    enumerable : true,
    configurable : true
})
Object.defineProperty(obj, "bbb", {
    value :10,
    writable : true,
    enumerable : true,
    configurable : true
});
                
console.log(obj.aaa)//0
console.log(obj.bbb)//10
obj.aaa = 7
console.log(obj.aaa)//7
console.log(obj.bbb)//17
//IE6+
var aValue = 0, bValue = 10;
var obj = {
    aaa: function(newValue){
        if(arguments.length){
            bValue += newValue
            aValue = newValue; 
            
        }
        console.log("xxxxxxxxx")
        return aValue
    },
    bbb: function(newValue){
        if(arguments.length){
            bValue = newValue; 
        }
          
        return bValue
    }
}
    
console.log(obj.aaa())//0
console.log(obj.bbb())//10
obj.aaa(7)
console.log(obj.aaa())//7
console.log(obj.bbb())//17

访问器又分四种,存在于ViewModel中的有三种。最简单的是属性访问器,它是对Model中某一个属性进行操作,相当于ko的监控属性。如果一个字段由模型中的两个属性,或两个以上,或要对这属性进行一下加工才产生它的值呢,这就要用到组合访问器,相当于ko的依赖监控属性,或emberjs中的computed。像程序中许多表示状态的中间量都可以抽象成一个组合访问器。组合访问器换言之,对已有的东西重新组合而成的属性的监控函数。集合访问器,是Model中的数组进行监控,如果它发生排序增删,它会通知双向依赖链的两端来刷新自身。集合访问器是个特殊的数组,它的方法都被重写了,虽然用法一样,但调用了它们会同步到对应的节点区域上!

ViewModel中还存在一种叫命令的东西,打个比方,绑定器相当于MVC中的action,命令相当于helpers。它只是一个普通的函数,框架不会再对它加工。框架对命令与访问器的区分是,访问器是用$type 与 "$"+(new Date - 0)这两个属性。说得可能有点复杂,比如有个对象var model = {aaa:1, bbb:1},然后$.ViewModel( obj )就得到它对应的ViewModel了。

接着我们看绑定部分。要实现事件绑定。knouckoutjs实现如下:

<div>
    <div data-bind="event: { mouseover: enableDetails, mouseout: disableDetails }">
        Mouse over me
    </div>
    <div data-bind="visible: detailsEnabled">
        Details
    </div>
</div>
 
<script type="text/javascript">
    var viewModel = {
        detailsEnabled: ko.observable(false),
        enableDetails: function() {
            this.detailsEnabled(true);
        },
        disableDetails: function() {
            this.detailsEnabled(false);
        }
    };
    ko.applyBindings(viewModel);
</script>

avalon v3参考了rivetsjs的绑定语法,实现如下:

        <div>
            <div data-on-mouseover="enableDetails" data-on-mouseout="disableDetails" >
                Mouse over me
            </div>
            <div data-display="detailsEnabled">
                Details
            </div>
        </div>
        <script type="text/javascript">
            require("avalon,ready", function($) {
                var VM = $.MVVM.convert({
                    detailsEnabled: false,
                    enableDetails: function() {
                        VM.detailsEnabled(true);
                    },
                    disableDetails: function() {
                        VM.detailsEnabled(false);
                    }
                });
                $.MVVM.render(VM)
            })
        </script>

要实现循环绑定,knouckoutjs实现如下

<table>
    <thead>
        <tr><th>First name</th><th>Last name</th></tr>
    </thead>
    <tbody data-bind="foreach: people">
        <tr>
            <td data-bind="text: firstName"></td>
            <td data-bind="text: lastName"></td>
        </tr>
    </tbody>
</table>
 
<script type="text/javascript">
    ko.applyBindings({
        people: [
            { firstName: 'Bert', lastName: 'Bertington' },
            { firstName: 'Charles', lastName: 'Charlesforth' },
            { firstName: 'Denise', lastName: 'Dentiste' }
        ]
    });
</script>

avalon v3实现如下:(data-each-[item]-[index],item, index是可选,名字任取,只要符合变量命名规则就行)

<script type="text/javascript">
    require("avalon,ready", function($) {
        $.MVVM.render({
            people: [
                { firstName: 'Bert', lastName: 'Bertington' },
                { firstName: 'Charles', lastName: 'Charlesforth' },
                { firstName: 'Denise', lastName: 'Dentiste' }
            ]
        });

    })

</script> 
<table>
    <thead>
        <tr><th>First name</th><th>Last name</th></tr>
    </thead>
    <tbody data-each-p="people">
        <tr>
            <td data-text="p.firstName"></td>
            <td data-text="p.lastName"></td>
        </tr>
    </tbody>
</table>

avalon v3的优势在于,它完全DSL,我们可以通过点号来查找VM中某一个可用的访问器或命令,作为数据绑定的值。而且实现起来很简单,不需要像knouckoutjs那样编写复杂的JSON编译器。复杂的东西就难维护,不易升级。有关数据绑定以后我与一系列教程介绍它的。

在数据绑定中,我们借助于一种特殊的属性来指引MVVM干活,格式为data-binding-[param]-[param]。以“-”断开,第二个字符串为绑定器的名字,剩余的为它的参数。比如事件绑定,data-on-click。

在avalon v3中,它提供了以下默认绑定器,可以通过$.ViewModel.bindings访问到。v3弥合了v2的伤口,完美支持事件绑定与事件代理。

  • data-text
  • data-html
  • data-class
  • data-css-[class]
  • data-attr
  • data-value
  • data-display
  • data-on-[event]
  • data-enable
  • data-disable
  • data-options
  • data-each-[item]-[index]
  • data-with-[value]-[key]
  • data-if
  • data-unless

avalon v3会将这个属性的名字分解成绑定器与其他参数,再将它的值得到VM中对应的访问器与命令,最后把它们构建成一个叫DOM访问器的东西,作为双向绑定链的顶层,专门与DOM打交道。

在jQuery时代,ID是我们命中元素最可靠的基点,以此为起点八爪鱼般处理周遭的节点。行为层上,我们通过事件绑定,几乎可以用根据代理一切事件。但jQuery是函数式编程,状态如果在连续在多个回调中使用时,它就要写在回调外面。当然我们可以缓存于某个节点上(data),在另一个回调中通过选择器得到那个节点再重新data出来。但整体上,jQuery代码都是以事件分割成一段段,中间夹杂着一些中间量与处理函数。它们是否能很好工作完全看编程人员的技术水平了。在MVVM中,数据绑定与元素是一体的,因此绝没有偏差。处理交互上,事件以命令的新身份登场, 回调被集合管理于VM,状态也被收笼于VM中,我们不再为如何组织代码伤脑筋,所有都有章而循,新手接力也易上手。MVVM减少对选择器的依赖,将数据与操作绑定在坚固的支点上。

链接地址

过几天写些教程,介绍如何用。完!

posted on 2012-12-13 14:29  司徒正美  阅读(5062)  评论(7编辑  收藏  举报