JavaScript入门教程

JavaScript简介
JavaScript语法基础
JavaScript流程控制
JavaScript函数
面向对象编程
JavaScript事件
JavaScript DOM
正则表达式
JavaScript BOM
AJAX

专题分析

浏览器兼容性
JS优化
Web前端开发规范
编辑器推荐
总结和笔记

学习助手

对象参考手册
ECMAScript分析
数据中心
QQ交流群

JavaScript事件化模型

只要对象带有PubSub 接口,就可以称之为事件化对象。特殊情况出现在用于存储数据的对象因内容变化而发布事件时,这里用于存储数据的对象又称作模型。模型就是MVC(Model-View-Controller,模型-视图-控制器)中的那个M。MVC 三层架构设计模式在最近几年里已经成为JavaScript 编程中最热点的主题之一。MVC 的核心理念是应用程序应该以数据为中心,所以模型发生的事件会影响到DOM(即MVC 中的视图)和服务器(通过MVC 中的控制器而产生影响)。

我们先来看看人气爆棚的Backbone.js 框架。可以像这样创建一个新的Model(模型)对象:
style = new Backbone.Model(
    {font: 'Georgia'}
);
model 作为参数时只是代表了那个简单的可以传递的JSON 对象。
style.toJSON() // {"font": "Georgia"}
但不同于普通对象的是,这个model 对象会在发生变化时发布通知。
style.on('change:font', function(model, font) {
    alert('Thank you for choosing ' + font + '!');
});
老式的JavaScript 依靠输入事件的处理器直接改变DOM。新式的JavaScript 先改变模型,接着由模型触发事件而导致DOM 的更新。在几乎所有的应用程序中,这种关注层面的分离都会带来更优雅、更直观的代码。

模型事件的传播

作为最简形式,MVC 三层架构只包括相互联系的模型和视图:“如果模型是这样变化的,那么DOM 就要那样变化。”不过,MVC 三层架构最大的利好出现在change(变化)事件冒泡上溯数据树的时候。

不用再去订阅数据树每片叶子上发生的事件,而只需订阅数据树根和枝处发生的事件即可。

事件化模型的set/get方法
正如我们知道的,JavaScript确实没有一种每当对象变化时就触发事件的机制。因此请记住,事件化模型要想工作的话,必须要使用一些像Backbone.js之set/get这样的方法。
style.set({font: 'Palatino'}); // 触发器警报!
style.get('font'); // 结果为"Palatino"
style.font = 'Comic Sans'; // 未触发任何事件
style.font; // 结果为"Comic Sans"
style.get('font'); // 结果仍为"Palatino"
将来也许无需如此,前提是名为Object.observe的ECMAScript提案已经获得广泛接纳。

为此,Backbone 的Model 对象常常组织成Backbone 集合的形式,其本质是事件化数组。我们可以监听什么时候对这些数组增减了Model对象。Backbone 集合可以自动传播其内蕴Model 对象所发生的事件。

举个例子,假设有一个spriteCollection(精灵集合)集合对象包含了上百个Model 对象,这些Model 对象代表了要画在canvas(画布)元素上的一些东西。每当任意一个精灵发生变化,都需要重新绘制画布。我们不用逐个在那些精灵上附加redraw(重绘)函数作为change 事件的处理器,相反,只要写这样一行代码:
spriteCollection.on('change', redraw);
注意,集合事件的这种自动传播只能下传一层。Backbone 没有嵌套式集合这样的概念。不过,我们可以自行用Backbone 的trigger 方法来实现嵌套式集合的多层传播。有了多层传播机制之后,任意的Backbone 对象都可以触发任意的事件。

事件循环与嵌套式变化

从一个对象向另一个对象传播事件的过程提出了一些需要关注的问题。如果每次有个对象上的事件引发了一系列事件并最终对这个对象本身触发了相同的事件,则结果就是事件循环。如果这种事件循环还是同步的,那就造成了堆栈上溢。

然而在很多时候,变化事件的循环恰恰是我们想要的。最常见的情况就是双向绑定——两个模型的取值会彼此关联。假设我们想保证x 始终等于2 * y。
var x = new Backbone.Model({value: 0});
var y = new Backbone.Model({value: 0});
x.on('change:value', function(x, xVal) { y.set({value: xVal / 2}); });
y.on('change:value', function(y, yVal) { x.set({value: 2 * yVal}); });
你可能觉得当x 或y 的取值变化时,这段代码会导致无限循环。但实际上它相当安全,这要感谢Backbone 中的两道保险。
  • 当新值等于旧值时,set 方法不会导致触发change 事件。
  • 模型正处于自身的change 事件期间时,不会再触发change 事件。

第二道保险代表了一种自保哲学。假设模型的一个变化导致同一个模型又一次变化。由于第二次变化被“嵌套”在第一次变化内部,所以这次变化的发生悄无声息。外面的观察者没有机会回应这种静默的变化。

很明显,在Backbone 中维持双向数据绑定是一个挑战。而另一个重要的MVC 框架,即Ember.js,采用了一种完全不同的方式:双向绑定必须作显式声明。一个值发生变化时,另一个值会通过延时事件作异步更新。于是,在触发这个异步更新事件之前,应用程序的数据将一直处于不一致的状态。

多个事件化模型之间的数据绑定问题不存在简单的解决方案。在Backbone 中,有一种审慎绕过这个问题的途径就是silent 标志。如果在set 方法中添加了{silent:true}选项,则不会触发change 事件。因此,如果多个彼此纠结的模型需要同时进行更新,一个很好的解决方法就是悄无声息地设置它们的值。然后,当这些模型的状态已经一致时,才调用它们的change 方法以触发对应的事件。

事件化模型为我们带来了一种将应用状态变化转换为事件的直观方式。Backbone 及其他MVC 框架做的每件事都跟这些模型有关,这些模型的状态变化会触发DOM 和服务器进行更新。要想掌控客户端JavaScript 应用程序与日俱增的复杂度,运用事件化模型存储互斥数据是伟大长征的第一步。