数据绑定与监控
在业务开发的过程中,我们可能会大量使用DOM操作,这个过程很繁琐,但是有了AngularJS,基本上就可以解脱了,做到这一点的关键是数据绑定。那什么是数据绑定,怎样绑定呢?本节将从多种角度,选取业务开发过程中的各种场景来举例说明。
基于单一模型的界面同步
有时候,我们会有这样的需求,界面上有个输入框,然后有另外一个地方,要把这个文本原样显示出来,如果没有数据绑定,这个代码可能很麻烦了,比如说,我们要监听输入框的各种事件,键盘按键、复制粘贴等等,然后再把取得的值写入对应位置。但是如果有数据绑定,这个事情就非常简单。
注意,任意绑定到数据模型的输入元素,它的数据变更都会导致模型变更,比如说,我们有另外一个需求,两个输入框,我们想要在任意一个中输入的时候,另外一个的值始终跟它保持同步。如果用传统的方式,需要在两个输入框上都添加事件,但是有了数据绑定之后,这一切都很简单:
这样的代码就可以了,核心要素还是这个数据模型b。
到目前为止的两个例子都很简单,但可能有人有问题要问,因为我们什么js都没有写,这个a跟b是哪里来的,为什么就能起作用呢?对于这个问题,我来作个类比。
比如大家写js,都知道变量可以不声明就使用:
a = 1;
这时候a被赋值到哪里了呢,到了全局的window对象上,也就是说其实相当于:
window.a = 1;
在AngularJS里,变量和表达式都附着在一种叫做作用域(scope)的东西上,可以自己声明新的作用域,也可以不声明。每个Angular应用默认会有一个根作用域($rootScope),凡是没有预先声明的东西,都会被创建到它上面去。
作用域的相关概念,我们会在下一章里面讲述。在这里,我们只需要知道,如果在界面中绑定了未定义的某变量,当它被赋值的时候,就会自动创建到对应的作用域上去。
前面我们在例子中提到的{{}}这种符号,称为插值表达式,这里面的内容将会被动态解析,也可以不使用这种方式来进行绑定,Angular另有一个ng-bind指令用于做这种事情:
{{price + "(元)"}}
当然我们这个例子并不好,因为,其实你可以把无关的数据都放在绑定表达式的外面,就像这样:
{{price}}(元)
那么,考虑个稍微复杂一些的。我们经常会遇到,在界面上展示性别,但是数据库里面存的是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 = "男";
这样的做法虽然能够达到效果,但破坏了模型的结构,我们可以做些改变:
if (gender == 1)
return "男";
}
};
这样我们就达到了目的。这个例子让我们发现,原来,在绑定表达式里面,是可以使用函数的。像我们这里的格式化函数,其实作用只存在于视图层,所以不会影响到真实数据模型。
注意:这里有两个注意点。
第一,在绑定表达式里面,只能使用自定义函数,不能使用原生函数。举个例子:
数组与对象结构的绑定
有时候,我们的数据并不总是这么简单,比如说,有可能会需要把一个数组的数据展示出来,这种情况下可以使用Angular的ng-repeat指令来处理,这个东西相当于一个循环,比如我们来看这段例子:
$scope.arr1 = [1, 2, 3];
$scope.add = function() {
$scope.arr1.push($scope.arr1.length + 1);
};
有时候,我们会遇到数组里有重复元素的情况,这时候,ng-repeat代码不能起作用,原因是Angular默认需要在数组中使用唯一索引,那假如我们的数据确实如此,怎么办呢?可以指定它使用序号作索引,就像这样:
$scope.arr2 = [1, 1, 3];
$scope.arr3 = [
[11, 12, 13],
[21, 22, 23],
[31, 32, 33]
];
$scope.arr4 = [{
name: "Tom",
age: 5
}, {
name: "Jerry",
age: 2
}];
Name | Age |
---|---|
{{child.name}} | {{child.age}} |
$scope.obj = {
a: 1,
b: 2,
c: 3
};
数据监控
有时候,我们不是直接把数据绑定到界面上,而是先要赋值到其他变量上,或者针对数据的变更,作出一些逻辑的处理,这个时候就需要使用监控。
最基本的监控很简单:
$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;
};
}
除了使用ng-class,还可以使用ng-style来对样式作更细致的控制,比如:
利用数据绑定,我们可以很容易实现原有的一些显示隐藏功能。比如说,当列表项有选中的时候,某些按钮出现,当什么都没选的时候,不出现这些按钮。
主要的代码部分还是借用上面那个列表,只添加一些相关的东西:
把这个代码放在刚才的列表旁边,位于同一个controller下,点击列表元素,就能看到绑定状态了。
有时候,我们也想控制按钮的可点击状态,比如刚才的例子,那两个按钮直接显示隐藏,太突兀了,我们来把它们改成启用和禁用。
同理,如果是输入框,可以用同样的方式,使用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;
};
}
所以,我们现在看到的是,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";
};
}
数据联动
在做实际业务的过程中,很容易就碰到数据联动的场景,最典型的例子是省市县的三级联动。很多前端教程或者基础面试题以此为例,综合考察其中所运用到的知识点。
如果是用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;
};
}
如果是绑定到下拉框上,代码更简单,因为AngularJS专门作了这种考虑,ng-options就是这样的设置:
从这个例子我们看到,相比于传统前端开发方式那种手动监听事件,手动更新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代码如下:
$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;
}
看,就这么简单,一个冒泡排序算法的可视化过程就写好啦。
甚至,AngularJS还允许我们在SVG中使用数据绑定,使用它,做一些小游戏也是很容易的,比如我写了个双人对战的象棋,这里有演示地址,可以查看源码,是不是很简单?
小结
刚才我们已经看到数据绑定的各种使用场景了,这个东西带给我们的最大好处是什么呢?我们回顾一下之前写Web界面,有一部分时间在写HTML和CSS,一部分时间在写纯逻辑的JavaScript,还有很多时间在把这两者结合起来,比如各种创建或选取DOM,设置属性,添加等等,这些事情都是很机械而繁琐的,数据绑定把这个过程简化了,代码也跟着清晰了。
数据绑定是一种思维方式,一切的核心就是数据。数据的变更导致界面的更新,如果我们想要更新界面,只需改变数据即可。