记:该文为转载,最近在使用NG开发DCOS管理平台
数据绑定与监控
在业务开发的过程中,我们可能会大量使用DOM操作,这个过程很繁琐,但是有了AngularJS,基本上就可以解脱了,做到这一点的关键是数据绑定。那什么是数据绑定,怎样绑定呢?本节将从多种角度,选取业务开发过程中的各种场景来举例说明。
基于单一模型的界面同步
有时候,我们会有这样的需求,界面上有个输入框,然后有另外一个地方,要把这个文本原样显示出来,如果没有数据绑定,这个代码可能很麻烦了,比如 说,我们要监听输入框的各种事件,键盘按键、复制粘贴等等,然后再把取得的值写入对应位置。但是如果有数据绑定,这个事情就非常简单。
<input type="text" ng-model="a"/> <div>{{a}}</div>
这么小小一段代码,就实现了我们想要的功能,是不是很可爱?这中间的关键是什么呢,就是变量a,在这里,变量a充当了数据模型的角色,输入框的数据变更会同步到模型上,然后再分发给绑定到这个模型的其他UI元素。
注意,任意绑定到数据模型的输入元素,它的数据变更都会导致模型变更,比如说,我们有另外一个需求,两个输入框,我们想要在任意一个中输入的时候,另外一个的值始终跟它保持同步。如果用传统的方式,需要在两个输入框上都添加事件,但是有了数据绑定之后,这一切都很简单:
<input type="text" ng-model="b"/> <input type="text" ng-model="b"/>
这样的代码就可以了,核心要素还是这个数据模型b。
到目前为止的两个例子都很简单,但可能有人有问题要问,因为我们什么js都没有写,这个a跟b是哪里来的,为什么就能起作用呢?对于这个问题,我来作个类比。
比如大家写js,都知道变量可以不声明就使用:
a = 1;
这时候a被赋值到哪里了呢,到了全局的window对象上,也就是说其实相当于:
window.a = 1;
在AngularJS里,变量和表达式都附着在一种叫做作用域(scope)的东西上,可以自己声明新的作用域,也可以不声明。每个Angular应用默认会有一个根作用域($rootScope),凡是没有预先声明的东西,都会被创建到它上面去。
作用域的相关概念,我们会在下一章里面讲述。在这里,我们只需要知道,如果在界面中绑定了未定义的某变量,当它被赋值的时候,就会自动创建到对应的作用域上去。
前面我们在例子中提到的{{}}这种符号,称为插值表达式,这里面的内容将会被动态解析,也可以不使用这种方式来进行绑定,Angular另有一个ng-bind指令用于做这种事情:
<input ng-model="a"/> <div>{{a}}</div> <div ng-bind="a"></div>
对模型的二次计算
嗯,有时候,实际情况没有这么简单,比如说,我们可能会需要对数据作一点处理,比如,在每个表示价格的数字后面添加一个单位:
<input type="number" ng-model="price"/> <span>{{price + "(元)"}}</span>
当然我们这个例子并不好,因为,其实你可以把无关的数据都放在绑定表达式的外面,就像这样:
<input type="number" ng-model="price"/> <span>{{price}}(元)</span>
那么,考虑个稍微复杂一些的。我们经常会遇到,在界面上展示性别,但是数据库里面存的是0或者1,那么,总要对它作个转换。有些比较老土的做法是这样,在模型上添加额外的字段给显示用:
这是原始数据:
var tom = { name: "Tom", gender: 1 };
被他转换之后,成了这样:
var tom = { name: "Tom", gender: 1, genderText: "男" };
if (person.gender == 0) person.genderText = "女"; if (person.gender == 1) person.genderText = "男";
这样的做法虽然能够达到效果,但破坏了模型的结构,我们可以做些改变:
<div>{{formatGender(tom.gender)}}</div>
$scope.formatGender = function(gender) { if (gender == 0) return "女"; if (gender == 1) return "男"; } };
这样我们就达到了目的。这个例子让我们发现,原来,在绑定表达式里面,是可以使用函数的。像我们这里的格式化函数,其实作用只存在于视图层,所以不会影响到真实数据模型。
注意:这里有两个注意点。
第一,在绑定表达式里面,只能使用自定义函数,不能使用原生函数。举个例子:
<div>{{Math.abs(-1)}}</div>
这句就是有问题的,因为Angular的插值表达式机制决定了它不能使用这样的函数,它是直接用自己的解释器去解析这个字符串的,如果确实需要调用原生函数,可以用一个自定义函数作包装,在自定义函数里面可以随意使用各种原生对象,就像这样:
<div>{{abs(-1)}}</div>
$scope.abs = function(number) { return Math.abs(number); };
第二,刚才我们这个例子只是为了说明可以这么用,但不表示这是最佳方案。Angular为这类需求提供了一种叫做filter的方案,可以在插值表达式中使用管道操作符来格式化数据,这个我们后面再细看。
数组与对象结构的绑定
有时候,我们的数据并不总是这么简单,比如说,有可能会需要把一个数组的数据展示出来,这种情况下可以使用Angular的ng-repeat指令来处理,这个东西相当于一个循环,比如我们来看这段例子:
$scope.arr1 = [1, 2, 3]; $scope.add = function() { $scope.arr1.push($scope.arr1.length + 1); };
<button ng-click="add()">Add Item</button> <ul> <li ng-repeat="item in arr1">{{item}}</li> </ul>
这样就可以把数组的内容展示到界面上了。数组中的数据产生变化时,也能够实时更新到界面上来。
有时候,我们会遇到数组里有重复元素的情况,这时候,ng-repeat代码不能起作用,原因是Angular默认需要在数组中使用唯一索引,那假如我们的数据确实如此,怎么办呢?可以指定它使用序号作索引,就像这样:
$scope.arr2 = [1, 1, 3];
<ul> <li ng-repeat="item in arr2 track by $index">{{item}}</li> </ul>
也可以把多维数组用多层循环的方式迭代出来:
$scope.arr3 = [ [11, 12, 13], [21, 22, 23], [31, 32, 33] ];
<ul> <li ng-repeat="childArr in arr3 track by $index"> {{$index}} <ul> <li ng-repeat="item in childArr track by $index">{{item}}</li> </ul> </li> </ul>
如果是数组中的元素是对象结构,也不难,我们用个表格来展示这个数组:
$scope.arr4 = [{ name: "Tom", age: 5 }, { name: "Jerry", age: 2 }];
<table class="table table-bordered"> <thead> <tr> <th>Name</th> <th>Age</th> </tr> </thead> <tbody> <tr ng-repeat="child in arr4"> <td>{{child.name}}</td> <td>{{child.age}}</td> </tr> </tbody> </table>
有时候我们想遍历对象的属性,也可以使用ng-repeat指令:
$scope.obj = { a: 1, b: 2, c: 3 };
<ul> <li ng-repeat="(key,value) in obj">{{key}}: {{value}}</li> </ul>
注意,在ng-repeat表达式里,我们使用了一个(key,value)来描述键值关系,如果只想要值,也可以不用这么写,直接按照数组的写法即可。对象值有重复的话,不用像数组那么麻烦需要指定$index做索引,因为它是对象的key做索引,这是不会重复的。
数据监控
有时候,我们不是直接把数据绑定到界面上,而是先要赋值到其他变量上,或者针对数据的变更,作出一些逻辑的处理,这个时候就需要使用监控。
最基本的监控很简单:
$scope.a = 1; $scope.$watch("a", function(newValue, oldValue) { alert(oldValue + " -> " + newValue); }); $scope.changeA = function() { $scope.a++; };
对作用域上的变量添加监控之后,就可以在变更时得到通知了。如果说新赋值的变量跟原先的相同,这个监控就不会被执行。比如说刚才例子中,继续对a赋值为1,不会进入监控函数。
以上这种方式可以监控到最直接的赋值,包括各种基本类型,以及复杂类型的引用赋值,比如说下面这个数组被重新赋值了,就可以被监控到:
$scope.arr = [0]; $scope.$watch("arr", function(newValue) { alert("change:" + newValue.join(",")); }); $scope.changeArr = function() { $scope.arr = [7, 8]; };
但这种监控方式只能处理引用相等的判断,对于一些更复杂的监控,需要更细致的处理。比如说,我们有可能需要监控一个数组,但并非监控它的整体赋值,而是监控其元素的变更:
$scope.$watch("arr", function(newValue) { alert("deep:" + newValue.join(",")); }, true); $scope.addItem = function() { $scope.arr.push($scope.arr.length); };
注意,这里我们在$watch函数中,添加了第三个参数,这个参数用于指示对数据的深层监控,包括数组的子元素和对象的属性等等。
样式的数据绑定
刚才我们提到的例子,都是跟数据同步、数据展示相关,但数据绑定的功能是很强大的,其应用场景取决于我们的想象力。
不知道大家有没有遇到过这样的场景,有一个数据列表,点中其中某条,这条就改变样式变成加亮,如果用传统的方式,可能要添加一些事件,然后在其中作一些处理,但使用数据绑定,能够大幅简化代码:
function ListCtrl($scope) { $scope.items = []; for (var i=0; i<10; i++) { $scope.items.push({ title:i }); } $scope.selectedItem = $scope.items[0]; $scope.select = function(item) { $scope.selectedItem = item; }; }
<ul class="list-group" ng-controller="ListCtrl"> <li ng-repeat="item in items" ng-class="{true:'list-group-item active',false: 'list-group-item'}[item==selectedItem]" ng-click="select(item)"> {{item.title}} </li> </ul>
在本例中,我们使用了一个循环来迭代数组中的元素,并且使用一个变量selectedItem用于标识选中项,然后关键点在于这个ng-class 的表达式,它能够根据当前项是否为选中项,作出一个判断,生成对应的样式名。这是绑定的一个典型应用了,基于它,能把一些之前需要依赖于某些控件的功能用 特别简单的方式做出来。
除了使用ng-class,还可以使用ng-style来对样式作更细致的控制,比如:
<input type="number" ng-model="x" ng-init="x=12"/> <div ng-style="{'font-size': x+'pt'}"> 测试字体大小 </div>
状态控制
有时候,我们除了控制普通的样式,还有可能要控制某个界面元素的显示隐藏。我们用ng-class或者ng-style当然都是可以控制元素的显示 隐藏的,但Angular给我们提供了一种快捷方式,那就是ng-show和ng-hide,它们是相反的,其实只要一个就可以了,提供两个是为了写表达 式的方便。
利用数据绑定,我们可以很容易实现原有的一些显示隐藏功能。比如说,当列表项有选中的时候,某些按钮出现,当什么都没选的时候,不出现这些按钮。
<button ng-show="selectedItem">有选中项的时候可以点我</button> <button ng-hide="selectedItem">没有选中项的时候可以点我</button>
把这个代码放在刚才的列表旁边,位于同一个controller下,点击列表元素,就能看到绑定状态了。
有时候,我们也想控制按钮的可点击状态,比如刚才的例子,那两个按钮直接显示隐藏,太突兀了,我们来把它们改成启用和禁用。
<button ng-disabled="!selectedItem">有选中项的时候可以点我</button> <button ng-disabled="selectedItem">没有选中项的时候可以点我</button>
同理,如果是输入框,可以用同样的方式,使用ng-readonly来控制其只读状态。
流程控制
除了使用ng-show和ng-hide来控制元素的显示隐藏,还可以使用ng-if,但这个的含义与实现机制都大为不同。所谓的show和 hide,意味着DOM元素已经存在,只是控制了是否显示,而if则起到了流程控制的作用,只有符合条件的DOM元素才会被创建,否则不创建。
比如下面的例子:
function IfCtrl($scope) { $scope.condition = 1; $scope.change = function() { $scope.condition = 2; }; }
<div ng-controller="IfCtrl"> <ul> <li ng-if="condition==1">if 1</li> <li ng-if="condition==2">if 2</li> <li ng-show="condition==1">show 1</li> <li ng-show="condition==2">show 2</li> </ul> <button ng-click="change()">change</button> </div>
这个例子初始的时候,创建了三个li,其中一个被隐藏(show 2),当点击按钮,condition变成2,仍然是三个li,其中,if 1没有了,if 2创建出来了,show 1隐藏了,show 2显示了。
所以,我们现在看到的是,if的节点是动态创建的。与此类似,我们还可以使用ng-switch指令:
function SwitchCtrl($scope) { $scope.condition = ""; $scope.a = function() { $scope.condition = "A"; }; $scope.b = function() { $scope.condition = "B"; }; $scope.c = function() { $scope.condition = "C"; }; }
<div ng-controller="SwitchCtrl"> <div ng-switch="condition"> <div ng-switch-when="A">A</div> <div ng-switch-when="B">B</div> <div ng-switch-default>default</div> </div> <button ng-click="a()">A</button> <button ng-click="b()">B</button> <button ng-click="c()">C</button> </div>
这个例子跟if基本上是一个意思,只是语法更自然些。
数据联动
在做实际业务的过程中,很容易就碰到数据联动的场景,最典型的例子是省市县的三级联动。很多前端教程或者基础面试题以此为例,综合考察其中所运用到的知识点。
如果是用Angular做开发,很可能这个就不成其为一个考点了,因为实现起来非常容易。
我们刚才已经实现了一个单级列表,可以借用这段代码,做两个列表,第一个的数据变动,对第二个的数据产生过滤。
function RegionCtrl($scope) { $scope.provinceArr = ["江苏", "云南"]; $scope.cityArr = []; $scope.$watch("selectedProvince", function(province) { // 真正有用的代码在这里,实际场景中这里可以是调用后端服务查询的关联数据 switch (province) { case "江苏": { $scope.cityArr = ["南京", "苏州"]; break; } case "云南": { $scope.cityArr = ["昆明", "丽江"]; break; } } }); $scope.selectProvince = function(province) { $scope.selectedProvince = province; }; $scope.selectCity = function(city) { $scope.selectedCity = city; }; }
<div ng-controller="RegionCtrl"> <ul class="list-group"> <li ng-repeat="province in provinceArr" ng-class="{true:'list-group-item active',false: 'list-group-item'}[province==selectedProvince]" ng-click="selectProvince(province)"> {{province}} </li> </ul> <ul class="list-group"> <li ng-repeat="city in cityArr" ng-class="{true:'list-group-item active',false: 'list-group-item'}[city==selectedCity]" ng-click="selectCity(city)"> {{city}} </li> </ul> </div>
这段代码看起来比刚才复杂一些,其实有价值的代码就那个$watch里面的东西。这是什么意思呢?意思是,监控selectedProvince这个变量,只要它改变了,就去查询它可能造成的更新数据,然后剩下的事情就不用我们管了。
如果是绑定到下拉框上,代码更简单,因为AngularJS专门作了这种考虑,ng-options就是这样的设置:
<select class="form-control col-md-6" ng-model="selectedProvince" ng-options="province for province in provinceArr"></select> <select class="form-control col-md-6" ng-model="selectedCity" ng-options="city for city in cityArr"></select>
从这个例子我们看到,相比于传统前端开发方式那种手动监听事件,手动更新DOM的方式,使用数据绑定做数据联动简直太容易了。如果要把这个例子改造成三级联动,只需对selectedCity也做一个监控就行了。
一个综合的例子
了解了这些细节之后,我们可以把它们结合起来做一个比较实际的综合例子。假设我们在为一家小店创建雇员的管理界面,其中包含一个雇员表格,以及一个可用于添加或编辑雇员的表单。
雇员包含如下字段:姓名,年龄,性别,出生地,民族。其中,姓名通过输入框输入字符串,年龄通过输入框输入整数,性别通过点选单选按钮来选择,出生地用两个下拉框选择省份和城市,民族可以选择汉族和少数民族,如果选择了少数民族,可以手动输入民族名称。
这个例子恰好能把我们刚才讲的绑定全部用到。我们先来看看有哪些绑定关系:
- 雇员表格可以选中某行,该行样式会高亮
- 如果选中了某行,其详细数据将会同步到表单上
- 如果点击过新增或修改按钮,当前界面处于编辑中,则表单可输入,否则表单只读
- 修改和删除按钮的可点击状态,取决于表格中是否有选中的行
- 出生地点的省市下拉框存在联动关系
- 民族名称的输入框,其可见性取决于选择了汉族还是少数民族的单选按钮
- 新增修改删除、确定取消,这两组按钮互斥,永远不同时出现,其可见性取决于当前是否正在编辑。
- 确定按钮的可点击状态,取决于当前表单数据是否合法
如果想做得精细,还有更多可以使用绑定的地方,不过上面这些已经足够我们把所有知识用一遍了。
这个例子的代码就不贴了,可以自行查看。
数据绑定的拓展运用
现在我们学会了数据绑定,可以借助这种特性,完成一些很别致的功能。比如说,如果想在页面上用div模拟一个正弦波,只需要把波形数据生成出来,然后一个绑定就可以完成了。
$scope.staticItems = []; for (var i=0; i<720; i++) { $scope.staticItems.push(Math.ceil(100 * (1 + Math.sin(i * Math.PI / 180)))); }
如果我们想让这个波形动起来,也很容易,只需要结合一个定时器,动态生成这个波形数据就可以了。为了形成滚动效果,当波形采点数目超过某个值的时候,可以把最初的点逐个拿掉,保持总的数组长度。
$scope.dynamicItems = []; var counter = 0; function addItem() { var newItem = Math.ceil(100 * (1 + Math.sin((counter++) * Math.PI / 180))); if ($scope.dynamicItems.length > 500) { $scope.dynamicItems.splice(0, 1); } $scope.dynamicItems.push(newItem); $timeout(function () { addItem(); }, 10); } addItem();
这个例子对应的HTML代码如下:
<div ng-controller="WaveCtrl"> <style> .wave-item { float: left; width: 1px; background-color: #ffab51; } </style> <div> <div ng-repeat="item in staticItems track by $index" class="wave-item" ng-style="{'height': item+'px'}"></div> </div> <div style="clear: left"> <div ng-repeat="item in dynamicItems track by $index" class="wave-item" ng-style="{'height': item+'px'}"></div> </div> </div>
有时候我们经常看到一些算法可视化的项目,比如把排序算法用可视化的方式展现出来,如果使用AngularJS的数据绑定,实现这种效果可谓易如反掌:
<div ng-controller="SortCtrl"> <style> .data-item { float: left; width: 20px; background-color: #c0c0c0; border: 1px solid #080808; } </style> <button ng-click="sort()">Sort</button> <div> <div ng-repeat="item in arr track by $index" class="data-item" ng-style="{'height': item*5+'px'}"></div> </div> </div>
$scope.arr = [2, 4, 5, 63, 55, 43]; $scope.sort = function () { if (!sort($scope.arr)) { $timeout(function() { $scope.sort(); }, 500); } }; function sort(array) { // 喵的,写到这个才发现yield是多么好啊 for (var i = 0; i < array.length; i++) { for (var j = array.length; j > 0; j--) { if (array[j] < array[j - 1]) { var temp = array[j - 1]; array[j - 1] = array[j]; array[j] = temp; return false; } } } return true; }
看,就这么简单,一个冒泡排序算法的可视化过程就写好啦。
查看原文:http://www.zoues.com/index.php/2016/05/12/dcos-angularjs-1/