人们总是爱探求完美的东西,医学界追求长生不死之药,炼金术师追求贤者之石,物理学家追求永动机……编程界也有自己的追求,完美架构什么的,什么从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的伤口,完美支持事件绑定与事件代理。
avalon v3会将这个属性的名字分解成绑定器与其他参数,再将它的值得到VM中对应的访问器与命令,最后把它们构建成一个叫DOM访问器的东西,作为双向绑定链的顶层,专门与DOM打交道。
在jQuery时代,ID是我们命中元素最可靠的基点,以此为起点八爪鱼般处理周遭的节点。行为层上,我们通过事件绑定,几乎可以用根据代理一切事件。但jQuery是函数式编程,状态如果在连续在多个回调中使用时,它就要写在回调外面。当然我们可以缓存于某个节点上(data),在另一个回调中通过选择器得到那个节点再重新data出来。但整体上,jQuery代码都是以事件分割成一段段,中间夹杂着一些中间量与处理函数。它们是否能很好工作完全看编程人员的技术水平了。在MVVM中,数据绑定与元素是一体的,因此绝没有偏差。处理交互上,事件以命令的新身份登场, 回调被集合管理于VM,状态也被收笼于VM中,我们不再为如何组织代码伤脑筋,所有都有章而循,新手接力也易上手。MVVM减少对选择器的依赖,将数据与操作绑定在坚固的支点上。
过几天写些教程,介绍如何用。完!