迷你MVVM框架 avalon的魔术
本文将深入介绍一下avalon的运作机理及与jQuery的区别。
许多人都知道MVVM是MVC的一个变体,但那些MVC是在后端的,包括微软的WPF,这意味着这个V与我们前端接触到的V差别很大。后端的V就是使用各种模板拼凑成一个静态页面给前端。而前端的V在JSer的眼中就是一个巨大的DOM树,要考虑加载时间,渲染顺序,浏览器对HTML的容错与修复,浏览器的默认事件,新图片与节点增删移动引起的reflow,是活生生的。前端的V就是一个DOM的世界,而后端就是字符串!
M,模型,一个数据体,用于填允我们的页面,通常PHP交给我们时数据已经填好了,想改变,就要再发出请求,然后通过jQuery寻找节点,修改它的innHTML与innerText。当然DOM操作是非常繁锁的,什么透明度,背景,位置等CSS属性我们都需要通过操作类名来修改。我们有时忙乎于这些细节,而把我们业务混在其中了。因此调试时,业务好了,但样式坏了,样好修好,这时务业又爆开了……
在MVVM中,M只是一个过客,它与其他表示业务状态的东西融入VM(ViewModel)中。ViewModel是一个状态的集合,当然还拖家带口监控着大量的回调。状态听起来是个深奥的概念,其实就是一个开关。比如if(aaa){}语句中的aaa,就是表示true与false,switch(bbb){}语句,它表示有多个值的状态,就像有人幼年,童年,少年,青年,中年,老年这几个阶段,它们都指向同一个东西。jQuery是什么管理状态的呢?比如我做个切换卡,点一下,这个面板显示,其他面板隐藏,它里面就使用了大量的hasClass来检测元素有没有某个表示显示的类名,有就隐藏,其他关闭。
$(button).click(function(){ var index = $(this).index(); $(panel).eq(index).show().siblings().hidden() })
而在MVVM中,只要妥善绑定好视图,直接像 vm.activated = 1,1 为你要显示的按钮,这样切换卡就切换过去,包括你点的按钮上的类名。你可以被动地在事件回调修改VM中的属性,也可以直接对VM中属性,无论哪一种,基本上很少看到DOM代码。
<div ms-controller="tabs" > <div ms-each-tab="tabs" class="triggers"> <a href="###" class="trigger" ms-class-selected="activated === $index" ms-click="activate">{{tab.title}}</a> </div> <div ms-each-tab="tabs" > <div class="panel" ms-visible="activated === $index">{{tab.panel}}</div> </div > </div >
avalon.ready(function() { avalon.define("tabs", function(a) { a.tabs = [ //tabs上的title, panel来源自后端的数据库,如果PHPer帮你把整个页面搞定,这个就不需要 { //只要下面的activated 与 activate title: "aaaa", panel: "aaaa panel" }, { title: "bbbb", panel: "ffffffffffffffffffff" }, { title: "cccc", panel: "cccc panel" } ] a.activated = 0 //这个是我们的务业产生的变量, a.activate = function(){ a.activated = this.$scope.$index;//这是我们的业务,就是改一下activated 的值。 } }) avalon.scan(); })
在MVVM中,数据是核心。而jQuery则以DOM为核心。而DOM只是HTML在JS的世界的抽象,是一个很易变的东西。因此如果业务代码遍历选择器表达式会非常难维护。但不可否认,jQuery是操作DOM的王者,让我们操作DOM顺手拈来。但如果不让你操作DOM,不是更好吗?就像jQuery不让你用getElementById,getElementsByTagName, querySelecterAll,大家都不知道里面有多少坑,短短几个字母$(expr)是背后sizzle选择器引擎1700行的实现!!!!jQuery其实是在用户代码与原生API中提供一层厚厚的粘合层,因此摸起来光溜溜。在MVVM中,DOM操作基本是水下运作了。由于VM与V之间的双向绑定,操作了VM中的数据(当然只能是监控属性),就会同步到DOM,我们透过DOM事件监控用户对DOM的改动,也会同步到VM。DOM隐形了,就像软件公司,到处跑出来活动的是业务员与不写代码的经理老总,程序员全部关起来加班!虽然这比喻有点残酷,但这正体现了各司其职的威力。能说会道去拉风投接单子没什么不妥,喜欢呆在电脑前的就让他呆吧。jQuery的世界就是一个混乱的公司,全能的程序员什么都做。
为了各司其职,必须有良好的分层。MVVM划分三层,M,VM,V,M是原始数据,用于转换为VM,VM管理状态与绑定回调,V通过绑定得到VM的状态与回调,渲染页面,绑定事件,切换类名,什么脏活都揽了——但用户只需要声明。avalon与其他前端MVVM框架最大的不同是,VM是用ecma262v5的新API, Object.defineProperties生成的一个充满访问器的对象,这样的对象,能通过用户对它的属性的读写,触发定义时的getter, setter函数。getter, setter对rubyer, pythoner, C#er应该很熟悉,我就不展开了。旧式IE,avalon利用VBScript的类实例,它也存在其他语言的访问器。不过,VBS对象不像JS对象那样随意添加新属性,删除已有属性,因此我们就无法监后添加的新属性。Object.defineProperties也一样,它能处理的属性也只是它定义时的属性,想监控后来的,需要再调用一次Object.defineProperties。尽管如此,也比其他MVVM框架魔幻多了。
如果监控属性不够用,可以用监控数组,计算属性与$watch, $fire方法。
比如下面一个监控数组fruits :
var model = avalon.define("form", [], function(vm) { vm.fruits = [{name: "xxx"}, {name: "yyy"}, {name: "ooo"}, {name: "ppp"}] vm.numbers = [1, 2, 3, 4] });
被改造成如下样子,我们可以在chrome的控制台看到它的正身:
![](https://images.cnblogs.com/cnblogs_com/rubylouvre/199042/o_observableArray.jpg)
首先这个数组的sort, splice, push, shift, reverse, pop, unshift等方法被重写,并添加了contains, ensure, remove, removeAll, removeAt, clear, upeate, set等方法, 还有表示它是个监控数组的isCollection, $id等属性。它内部四个对象也转换子ViewModel,name转换为监控属性,每个name在内部都有个set, get方法。我们只需要aaa.name = "xxx"; var d = aaa.name就能调用它们。而不像knockout那样全部转为真正的函数。
为了能互相同步,要写你绑定在页面上的字段为监控属性,如果它是一个复杂数据的某个子属性的子属性,那么至少也保持最后那个是监控属性,否则无法同步
var model = avalon.define("test", [], function(vm) { vm.fruits = [{name: "xxx"}, {name: "yyy"}, {name: "ooo"}, {name: "ppp"}] vm.numbers = [1, 2, 3, 4] vm.lang = "ruby" }); //在这里,我们可以监听当中的name 即{{ fruits[0].name }}, 但无法监控numbers中的元素,因为数字无法作为Object.defineProperty的第一个参数传入,因此转换不了。 // Object.defineProperty(object, propertyname, descriptor);//obj必须为对象
根据这思路,整个转换过程为:
Object.defineProperty(vm, "fruits", {set:fn, get: fn})//事实上,avalon是使用Object.defineProperties同时处理! Object.defineProperty(vm, "numbers ", {set:fn, get: fn}) Object.defineProperty(vm, "lang", {set:fn, get: fn}) Object.defineProperty(fruits[0], "name", {set:fn, get: fn}) Object.defineProperty(fruits[1], "name", {set:fn, get: fn}) Object.defineProperty(fruits[2], "name", {set:fn, get: fn}) Object.defineProperty(fruits[3], "name", {set:fn, get: fn})
比如像下面这样的复杂结构,avalon就有点为不从心了。因此不要直接把Model的数据整个扔到ViewModel中,你懒,avalon也懒——它只是监听对象属性的变化,数组长度与位置的变化。不会监听纯字符数组与纯字母数组。即便对于angular,google拥有这么多算法帝,它也要求你使用$watch来处理,而不是直接放在VM中就一了百了。emberjs, knockout就更呛了。要用好ViewModel必须对原始数据进行加工。
var model = avalon.define("class", [], function(vm) { vm.data = { rows: [ { "d": [ "PO00000078", "ABC" ] }, { "d": [ "PO00000079", "DEF" ] } ] } })
如果一定要监听,只能使用each绑定,你这个对象在层次结构上有多少个数组,就应该用多少次each绑定。比如这个对象,就有rows, d这个数组,需要用上两次。
<ul ms-each-row="rows" ms-controller="class"> <li ms-each-td="row.d"> {{td}} </li> </ul>
生成的结构如下:
![](https://images.cnblogs.com/cnblogs_com/rubylouvre/199042/t_xxxxx.jpg)
不过真正强大的是各种绑定,帮你打包所有DOM操作。可详看这里的API说明。
MVVM是前端未来的发展方向,微软有knockout, winjs等MVVM框架; 著名的.Net组件开发公司 Telerik 推出了一套基于 jQuery的MVVM UI库kendoui; 谷歌组织开发angular;jQuery, rails,Sproutecore,Merb,Handlebars这几个著名框架的核心成员,超级大牛Yehuda Katz推出了emberjs!
外国有文章介绍,使用了AngularJS(MVVM)代替(Backbone),代码减少了一半。
http://www.localytics.com/blog/2013/angularjs-at-localytics/
![](http://www.justaple.com/staples/public/f2ebbdcf6f4f11e09ab840404112cf76/2013/4/11/bc2b2a3ca28d11e2932140404112cf76/http/www.localytics.com/wp-content/uploads/2013/04/amp-angular-backbone-550x339.png)
现在园子里有不少人在用knockout与angular,在ruby china中,最爱玩新技术的rubyer最近也密集提到它们。毫无疑问,这是非常有吸引力,极具生产力的东西。
avalon作为现在最迷你的MVVM框架,易用性也堪称一绝,绝对可以一试。