上一篇:Angular沉思录(二)视图模型的层次

下一篇:[翻译]Angular的问题

数据绑定与监控

在业务开发的过程中,我们可能会大量使用DOM操作,这个过程很繁琐,但是有了AngularJS,基本上就可以解脱了,做到这一点的关键是数据绑定。那什么是数据绑定,怎样绑定呢?本节将从多种角度,选取业务开发过程中的各种场景来举例说明。

基于单一模型的界面同步

有时候,我们会有这样的需求,界面上有个输入框,然后有另外一个地方,要把这个文本原样显示出来,如果没有数据绑定,这个代码可能很麻烦了,比如说,我们要监听输入框的各种事件,键盘按键、复制粘贴等等,然后再把取得的值写入对应位置。但是如果有数据绑定,这个事情就非常简单。

这么小小一段代码,就实现了我们想要的功能,是不是很可爱?这中间的关键是什么呢,就是变量a,在这里,变量a充当了数据模型的角色,输入框的数据变更会同步到模型上,然后再分发给绑定到这个模型的其他UI元素。

注意,任意绑定到数据模型的输入元素,它的数据变更都会导致模型变更,比如说,我们有另外一个需求,两个输入框,我们想要在任意一个中输入的时候,另外一个的值始终跟它保持同步。如果用传统的方式,需要在两个输入框上都添加事件,但是有了数据绑定之后,这一切都很简单:

这样的代码就可以了,核心要素还是这个数据模型b。

到目前为止的两个例子都很简单,但可能有人有问题要问,因为我们什么js都没有写,这个a跟b是哪里来的,为什么就能起作用呢?对于这个问题,我来作个类比。

比如大家写js,都知道变量可以不声明就使用:

这时候a被赋值到哪里了呢,到了全局的window对象上,也就是说其实相当于:

在AngularJS里,变量和表达式都附着在一种叫做作用域(scope)的东西上,可以自己声明新的作用域,也可以不声明。每个Angular应用默认会有一个根作用域($rootScope),凡是没有预先声明的东西,都会被创建到它上面去。

作用域的相关概念,我们会在下一章里面讲述。在这里,我们只需要知道,如果在界面中绑定了未定义的某变量,当它被赋值的时候,就会自动创建到对应的作用域上去。

前面我们在例子中提到的{{}}这种符号,称为插值表达式,这里面的内容将会被动态解析,也可以不使用这种方式来进行绑定,Angular另有一个ng-bind指令用于做这种事情:

对模型的二次计算

嗯,有时候,实际情况没有这么简单,比如说,我们可能会需要对数据作一点处理,比如,在每个表示价格的数字后面添加一个单位:

当然我们这个例子并不好,因为,其实你可以把无关的数据都放在绑定表达式的外面,就像这样:

那么,考虑个稍微复杂一些的。我们经常会遇到,在界面上展示性别,但是数据库里面存的是0或者1,那么,总要对它作个转换。有些比较老土的做法是这样,在模型上添加额外的字段给显示用:

这是原始数据:

被他转换之后,成了这样:

转换函数内容如下:

这样的做法虽然能够达到效果,但破坏了模型的结构,我们可以做些改变:

这样我们就达到了目的。这个例子让我们发现,原来,在绑定表达式里面,是可以使用函数的。像我们这里的格式化函数,其实作用只存在于视图层,所以不会影响到真实数据模型。

注意:这里有两个注意点。

第一,在绑定表达式里面,只能使用自定义函数,不能使用原生函数。举个例子:

这句就是有问题的,因为Angular的插值表达式机制决定了它不能使用这样的函数,它是直接用自己的解释器去解析这个字符串的,如果确实需要调用原生函数,可以用一个自定义函数作包装,在自定义函数里面可以随意使用各种原生对象,就像这样:

第二,刚才我们这个例子只是为了说明可以这么用,但不表示这是最佳方案。Angular为这类需求提供了一种叫做filter的方案,可以在插值表达式中使用管道操作符来格式化数据,这个我们后面再细看。

数组与对象结构的绑定

有时候,我们的数据并不总是这么简单,比如说,有可能会需要把一个数组的数据展示出来,这种情况下可以使用Angular的ng-repeat指令来处理,这个东西相当于一个循环,比如我们来看这段例子:

这样就可以把数组的内容展示到界面上了。数组中的数据产生变化时,也能够实时更新到界面上来。

有时候,我们会遇到数组里有重复元素的情况,这时候,ng-repeat代码不能起作用,原因是Angular默认需要在数组中使用唯一索引,那假如我们的数据确实如此,怎么办呢?可以指定它使用序号作索引,就像这样:

也可以把多维数组用多层循环的方式迭代出来:

如果是数组中的元素是对象结构,也不难,我们用个表格来展示这个数组:

有时候我们想遍历对象的属性,也可以使用ng-repeat指令:

注意,在ng-repeat表达式里,我们使用了一个(key, value)来描述键值关系,如果只想要值,也可以不用这么写,直接按照数组的写法即可。对象值有重复的话,不用像数组那么麻烦需要指定$index做索引,因为它是对象的key做索引,这是不会重复的。

数据监控

有时候,我们不是直接把数据绑定到界面上,而是先要赋值到其他变量上,或者针对数据的变更,作出一些逻辑的处理,这个时候就需要使用监控。

最基本的监控很简单:

对作用域上的变量添加监控之后,就可以在变更时得到通知了。如果说新赋值的变量跟原先的相同,这个监控就不会被执行。比如说刚才例子中,继续对a赋值为1,不会进入监控函数。

以上这种方式可以监控到最直接的赋值,包括各种基本类型,以及复杂类型的引用赋值,比如说下面这个数组被重新赋值了,就可以被监控到:

但这种监控方式只能处理引用相等的判断,对于一些更复杂的监控,需要更细致的处理。比如说,我们有可能需要监控一个数组,但并非监控它的整体赋值,而是监控其元素的变更:

注意,这里我们在$watch函数中,添加了第三个参数,这个参数用于指示对数据的深层监控,包括数组的子元素和对象的属性等等。

样式的数据绑定

刚才我们提到的例子,都是跟数据同步、数据展示相关,但数据绑定的功能是很强大的,其应用场景取决于我们的想象力。

不知道大家有没有遇到过这样的场景,有一个数据列表,点中其中某条,这条就改变样式变成加亮,如果用传统的方式,可能要添加一些事件,然后在其中作一些处理,但使用数据绑定,能够大幅简化代码:

在本例中,我们使用了一个循环来迭代数组中的元素,并且使用一个变量selectedItem用于标识选中项,然后关键点在于这个ng-class的表达式,它能够根据当前项是否为选中项,作出一个判断,生成对应的样式名。这是绑定的一个典型应用了,基于它,能把一些之前需要依赖于某些控件的功能用特别简单的方式做出来。

除了使用ng-class,还可以使用ng-style来对样式作更细致的控制,比如:

状态控制

有时候,我们除了控制普通的样式,还有可能要控制某个界面元素的显示隐藏。我们用ng-class或者ng-style当然都是可以控制元素的显示隐藏的,但Angular给我们提供了一种快捷方式,那就是ng-show和ng-hide,它们是相反的,其实只要一个就可以了,提供两个是为了写表达式的方便。

利用数据绑定,我们可以很容易实现原有的一些显示隐藏功能。比如说,当列表项有选中的时候,某些按钮出现,当什么都没选的时候,不出现这些按钮。

主要的代码部分还是借用上面那个列表,只添加一些相关的东西:

把这个代码放在刚才的列表旁边,位于同一个controller下,点击列表元素,就能看到绑定状态了。

有时候,我们也想控制按钮的可点击状态,比如刚才的例子,那两个按钮直接显示隐藏,太突兀了,我们来把它们改成启用和禁用。

同理,如果是输入框,可以用同样的方式,使用ng-readonly来控制其只读状态。

流程控制

除了使用ng-show和ng-hide来控制元素的显示隐藏,还可以使用ng-if,但这个的含义与实现机制都大为不同。所谓的show和hide,意味着DOM元素已经存在,只是控制了是否显示,而if则起到了流程控制的作用,只有符合条件的DOM元素才会被创建,否则不创建。

比如下面的例子:

这个例子初始的时候,创建了三个li,其中一个被隐藏(show 2),当点击按钮,condition变成2,仍然是三个li,其中,if 1没有了,if 2创建出来了,show 1隐藏了,show 2显示了。

所以,我们现在看到的是,if的节点是动态创建的。与此类似,我们还可以使用ng-switch指令:

这个例子跟if基本上是一个意思,只是语法更自然些。

数据联动

在做实际业务的过程中,很容易就碰到数据联动的场景,最典型的例子是省市县的三级联动。很多前端教程或者基础面试题以此为例,综合考察其中所运用到的知识点。

如果是用Angular做开发,很可能这个就不成其为一个考点了,因为实现起来非常容易。

我们刚才已经实现了一个单级列表,可以借用这段代码,做两个列表,第一个的数据变动,对第二个的数据产生过滤。

这段代码看起来比刚才复杂一些,其实有价值的代码就那个$watch里面的东西。这是什么意思呢?意思是,监控selectedProvince这个变量,只要它改变了,就去查询它可能造成的更新数据,然后剩下的事情就不用我们管了。

如果是绑定到下拉框上,代码更简单,因为AngularJS专门作了这种考虑,ng-options就是这样的设置:

从这个例子我们看到,相比于传统前端开发方式那种手动监听事件,手动更新DOM的方式,使用数据绑定做数据联动简直太容易了。如果要把这个例子改造成三级联动,只需对selectedCity也做一个监控就行了。

一个综合的例子

了解了这些细节之后,我们可以把它们结合起来做一个比较实际的综合例子。假设我们在为一家小店创建雇员的管理界面,其中包含一个雇员表格,以及一个可用于添加或编辑雇员的表单。

雇员包含如下字段:姓名,年龄,性别,出生地,民族。其中,姓名通过输入框输入字符串,年龄通过输入框输入整数,性别通过点选单选按钮来选择,出生地用两个下拉框选择省份和城市,民族可以选择汉族和少数民族,如果选择了少数民族,可以手动输入民族名称。

这个例子恰好能把我们刚才讲的绑定全部用到。我们先来看看有哪些绑定关系:

  • 雇员表格可以选中某行,该行样式会高亮
  • 如果选中了某行,其详细数据将会同步到表单上
  • 如果点击过新增或修改按钮,当前界面处于编辑中,则表单可输入,否则表单只读
  • 修改和删除按钮的可点击状态,取决于表格中是否有选中的行
  • 出生地点的省市下拉框存在联动关系
  • 民族名称的输入框,其可见性取决于选择了汉族还是少数民族的单选按钮
  • 新增修改删除、确定取消,这两组按钮互斥,永远不同时出现,其可见性取决于当前是否正在编辑。
  • 确定按钮的可点击状态,取决于当前表单数据是否合法

如果想做得精细,还有更多可以使用绑定的地方,不过上面这些已经足够我们把所有知识用一遍了。

这个例子的代码就不贴了,可以自行查看。

数据绑定的拓展运用

现在我们学会了数据绑定,可以借助这种特性,完成一些很别致的功能。比如说,如果想在页面上用div模拟一个正弦波,只需要把波形数据生成出来,然后一个绑定就可以完成了。

如果我们想让这个波形动起来,也很容易,只需要结合一个定时器,动态生成这个波形数据就可以了。为了形成滚动效果,当波形采点数目超过某个值的时候,可以把最初的点逐个拿掉,保持总的数组长度。

这个例子对应的HTML代码如下:

有时候我们经常看到一些算法可视化的项目,比如把排序算法用可视化的方式展现出来,如果使用AngularJS的数据绑定,实现这种效果可谓易如反掌:

看,就这么简单,一个冒泡排序算法的可视化过程就写好啦。

甚至,AngularJS还允许我们在SVG中使用数据绑定,使用它,做一些小游戏也是很容易的,比如我写了个双人对战的象棋,这里有演示地址,可以查看源码,是不是很简单?

小结

刚才我们已经看到数据绑定的各种使用场景了,这个东西带给我们的最大好处是什么呢?我们回顾一下之前写Web界面,有一部分时间在写HTML和CSS,一部分时间在写纯逻辑的JavaScript,还有很多时间在把这两者结合起来,比如各种创建或选取DOM,设置属性,添加等等,这些事情都是很机械而繁琐的,数据绑定把这个过程简化了,代码也跟着清晰了。

数据绑定是一种思维方式,一切的核心就是数据。数据的变更导致界面的更新,如果我们想要更新界面,只需改变数据即可。

佛曰:命由己造,相由心生,世间万物皆是化相,心不动,万物皆不动,心不变,万物皆不变。

本章所涉及的所有Demo,参见在线演示地址

代码库

演讲幻灯片下载:点这里

上一篇:Angular沉思录(二)视图模型的层次

下一篇:[翻译]Angular的问题