AngularJS实例教程(一)——数据绑定与监控
AngularJS实例教程(二)——作用域与事件
数据绑定与监控
$watch
$scope.a = 1;$scope.$watch("a", function(newValue, oldValue) {alert(oldValue + " -> " + newValue);});$scope.changeA = function() {$scope.a++;};以上这种方式可以监控到最直接的赋值,包括各种基本类型,以及复杂类型的引用赋值,比如说下面这个数组被重新赋值了,就可以被监控到:***************************************************************$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的第三个参数true**
对某个选中的元素进行进一步处理
不知道大家有没有遇到过这样的场景,有一个数据列表,点中其中某条,这条就改变样式变成加亮,如果用传统的方式,可能要添加一些事件,然后在其中作一些处理,但使用数据绑定,能够大幅简化代码: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>
作用域与事件
作用域的继承关系
在开发过程中,我们可能会出现控制器的嵌套,看下面这段代码:<div ng-controller="OuterCtrl"><span>{{a}}</span><div ng-controller="InnerCtrl"><span>{{a}}</span></div></div>function OuterCtrl($scope) {$scope.a = 1;}function InnerCtrl($scope) {}注意结果,我们可以看到界面显示了两个1,而我们只在OuterCtrl的作用域里定义了a变量,但界面给我们的结果是,两个a都有值。这里内层的a值显然来自外层,因为当我们对界面作出这样的调整之后,就只有一个了:<div ng-controller="OuterCtrl"><span>{{a}}</span></div><div ng-controller="InnerCtrl"><span>{{a}}</span></div>这是为什么呢?在Angular中,如果两个控制器所对应的视图存在上下级关系,它们的作用域就自动产生继承关系。什么意思呢?先考虑在纯JavaScript代码中,两个构造函数各自有一个实例:function Outer() {this.a = 1;}function Inner() {}var outer = new Outer();var inner = new Inner();在这里面添加什么代码,能够让inner.a == 1呢?熟悉JavaScript原型的我们,当然毫不犹豫就加了一句:Inner.prototype = outer;function Outer() {this.a = 1;}function Inner() {}var outer = new Outer();Inner.prototype = outer;var inner = new Inner();于是就得到想要的结果了。再回到我们的例子里,Angular的实现机制其实也就是把这两个控制器中的$scope作了关联,外层的作用域实例成为了内层作用域的原型。以此类推,整个Angular应用的作用域,都存在自顶向下的继承关系,最顶层的是$rootScope,然后一级一级,沿着不同的控制器往下,形成了一棵作用域的树,这也就像封建社会:天子高高在上,分茅裂土,公侯伯子男,一级一级往下,层层从属。
简单变量的取值与赋值
既然作用域是通过原型来继承的,自然也就可以推论出一些特征来。比如说这段代码,点击按钮的结果是什么?<div ng-controller="OuterCtrl"><span>{{a}}</span><div ng-controller="InnerCtrl"><span>{{a}}</span><button ng-click="a=a+1">a++</button></div></div>function OuterCtrl($scope) {$scope.a = 1;}function InnerCtrl($scope) {}点了按钮之后,两个a不一致了,里面的变了,外面的没变,这是为什么?原先两层不是共用一个a吗,怎么会出现两个不同的值?看这句就能明白了,相当于我们之前那个例子里,这样赋值了:function Outer() {this.a = 1;}function Inner() {}var outer = new Outer();Inner.prototype = outer;var inner = new Inner();inner.a = inner.a + 1;最后这句,很有意思,它有两个过程,取值的时候,因为inner自身上面没有,所以沿着原型往上取到了1,然后自增了之后,赋值给自己,这个赋值的时候就不同了,敬爱的林副主席教导我们:有a就赋值,没有a,创造一个a也要赋值。(之前是原型里有a,加完一次之后自己有一个值为2的a。同时原型里还有个值为1的a)所以这么一来,inner上面就被赋值了一个新的a,outer里面的仍然保持原样,这也就导致了刚才看到的结果。初学者在这个问题上很容易犯错,如果不能随时很明确地认识到这些变量的差异,很容易写出有问题的程序。既然这样,我们可以用一些别的方式来减少变量的歧义。
对象在上下级作用域之间的共享
比如说,我们就是想上下级共享变量,不创建新的,该怎么办呢?考虑下面这个例子:function Outer() {this.data = {a: 1};}function Inner() {}var outer = new Outer();Inner.prototype = outer;var inner = new Inner();console.log(outer.data.a);console.log(inner.data.a);// 注意,这个时候会怎样?inner.data.a += 1;console.log(outer.data.a);console.log(inner.data.a);这次的结果就跟上次不同了,原因是什么呢?因为两者的data是同一个引用,对这个对象上面的属性修改,是可以反映到两级对象上的。我们通过引入一个data对象的方式,继续使用了原先的变量。把这个代码移植到AngularJS里,就变成了下面这样:<div ng-controller="OuterCtrl"><span>{{data.a}}</span><div ng-controller="InnerCtrl"><span>{{data.a}}</span><button ng-click="data.a=data.a+1">increase a</button></div></div>function OuterCtrl($scope) {$scope.data = {a: 1};}function InnerCtrl($scope) {}从这个例子我们就发现了,如果想要避免变量歧义,显式指定所要使用的变量会是比较好的方式,那么如果我们确实就是要在上下级分别存在相同的变量该怎么办呢,比如说下级的点击,想要给上级的a增加1,我们可以使用$parent来指定上级作用域。<div ng-controller="OuterCtrl"><span>{{a}}</span><div ng-controller="InnerCtrl"><span>{{a}}</span><button ng-click="$parent.a=a+1">increase a</button></div></div>function OuterCtrl($scope) {$scope.a = 1;}function InnerCtrl($scope) {}
不请自来的新作用域(ng-repeat)
在一个应用中,最常见的会创建作用域的指令是ng-controller,因为它会实例化一个新的控制器,往里面注入一个$scope,也就是一个新的作用域,所以一般人都会很自然地理解这里面的作用域隔离关系。但是对于另外一些情况,就有些困惑了,比如说,ng-repeat,怎么理解这个东西也会创建新作用域呢?还是看之前的例子:$scope.arr = [1, 2, 3];<ul><li ng-repeat="item in arr track by $index">{{item}}</li></ul>在ng-repeat的表达式里,有一个item,我们来思考一下,这个item是个什么情况。在这里,数组中有三个元素,在循环的时候,这三个元素都叫做item,这时候就有个问题,如何区分每个不同的item,可能我们这个例子还不够直接,那改一下:<div>outer: {{sum1}}</div><ul><li ng-repeat="item in arr track by $index">{{item}}<button ng-click="sum1=sum1+item">increase</button><div>inner: {{sum1}}</div></li></ul>这个例子运行一下,我们会发现每个item都会独立改变,说明它们确实是区分开了的。事实上,Angular在这里为ng-repeat的每个子项都创建了单独的作用域,所以,每个item都存在于自己的作用域里,互不影响。有时候,我们是需要在循环内部访问外层变量的,回忆一下,在本章的前面部分中,我们举例说,如果两个控制器,它们的视图有包含关系,内层控制器的作用域可以通过$parent来访问外层控制器作用域上的变量,那么,在这种循环里,是不是也可以如此呢?**疑惑,上面例子sum1未定义可以正常按0开始计算,且scope.sum1 = undefined后也能正常执行表达式**看这个例子:<div>outer: {{sum2}}</div><ul><li ng-repeat="item in arr track by $index">{{item}}<button ng-click="$parent.sum2=sum2+item">increase</button><div>inner: {{sum2}}</div></li></ul>果然是可以的。很多时候,人们会把$parent误认为是上下两级控制器之间的访问通道,但从这个例子我们可以看到,并非如此,只是**两级作用域**而已,作用域跟控制器还是不同的,刚才的循环可以说是有两级作用域,但都处于同一个控制器之中。刚才我们已经提到了ng-controller和ng-repeat这两个常用的内置指令,两者都会创建新的作用域,除此之外,还有一些其他指令也会创建新的作用域,很多初学者在使用过程中很容易产生困扰。第一章我们提到用ng-show和ng-hide来控制某个界面块的整体展示和隐藏,但同样的功能其实也可以用ng-if来实现。那么这两者的差异是什么呢,所谓show和hide,大家很好理解,就是某个东西原先有,只是控制是否显式,而if的含义是,如果满足条件,就创建这块DOM,否则不创建。所以,ng-if所控制的界面块,只有条件为真的时候才会存在于DOM树中。除此之外,两者还有个差异,ng-show和ng-hide是不自带作用域的,而ng-if则自己创建了一级作用域。在用的时候,两者就是有差别的,比如说内部元素访问外层定义的变量,就需要使用类似ng-repeat那样的$parent语法了。相似的类型还有ng-switch,ng-include等等,规律可以总结,也就是那些会动态创建一块界面的东西,都是自带一级作用域。
上面第一个例子的运行结果(点击两次后结果如图,第一次点击时分别是1,2,3)
__上面第二个例子的运行结果(所有sum2一起变化)
“悬空”的作用域
在任意一个已有的作用域上调用$new(),就能创建一个新的作用域:
_
var newScope = scope.$new();
newScope跟任何界面模板都不存在绑定关系,创建它的作用域会成为它的$parent。
这种作用域可以经过$compile阶段,与某视图模板进行融合。
理解**:**我们可以用DocumentFragment作类比,当作用域被创建的时候,就好比是创建了一个DocumentFragment,它是不在DOM树上的,只有当它被append到DOM树上,才能够被当做普通的DOM来使用。
如果我们想要监控一个数据的变化,但这个数据并非绑定到界面上的,比如下面这样,怎么办?
_
function IsolateCtrl($scope) {var child = {a: 1};child.a++;}
这里的child,它并未绑定到$scope上,如果我们想要在a变化的时候做某些事情,是没有办法做的。
注意:我们的$watch和$eval之类的方法,其实都是实现在作用域对象上的,即使作用域对象没有与界面产生关联,也依旧可以使用这些方法。
function IsolateCtrl($scope) {var child = $scope.$new();child.a = 1;child.$watch("a", function(newValue) {alert(newValue);});$scope.change = function() {child.a++;};}
这时候child里面a的变更就可以被观测到,并且,这个child只有本作用域可以访问到。
作用域上的事件($emit,$broadcast)

如果子视图A1想要发出一个业务事件,使得B1和B2能够得到通知,过程就会是:
- 沿着父作用域一路往上到达双方共同的祖先作用域
- 从祖先作用域一级一级往下进行广播,直到到达需要的地方
```javascript
从作用域往上发送事件,使用scope.$emit
$scope.$emit(“someEvent”, {});
从作用域往下发送事件,使用scope.$broadcast $scope.$broadcast(“someEvent”, {});
第二个参数是要随事件带出的数据
<a name="pzSXw"></a>### 事件的接收与阻止($on)_<br />_**无论是$emit还是$broadcast发送的事件,都可以被接收,接收这两种事件的方式是一样的:**_<br />_```javascript$scope.$on("someEvent", function(e) {// 这里从e上可以取到发送过来的数据});
注意:被接收不代表传播终止,依旧保持状态:
- $emit的事件将继续向上传播
- $broadcast的事件将继续向下传播
注意:有时候想让事件停下来,把事件中止,但是
- $emit发出的事件是可以被中止的
- $broadcast发出的不可以被中止
阻止$emit的传播可以用:
$scope.$on("someEvent", function(e) {e.stopPropagation();});
事件总线
当层级较多时使用$emit和$broadcast效率会较低,因此我们能不能这样:搞一个专门负责通讯的机构,大家的消息都发给它,然后由它发给相关人员,其他人员在理念上都是平级关系。
使用一个公共模块:
_
app.factory("EventBus", function() {var eventMap = {};var EventBus = {on : function(eventType, handler) {//multiple event listenerif (!eventMap[eventType]) {eventMap[eventType] = [];}eventMap[eventType].push(handler);},off : function(eventType, handler) {for (var i = 0; i < eventMap[eventType].length; i++) {if (eventMap[eventType][i] === handler) {eventMap[eventType].splice(i, 1);break;}}},fire : function(event) {var eventType = event.type;if (eventMap && eventMap[eventType]) {for (var i = 0; i < eventMap[eventType].length; i++) {eventMap[eventType][i](event);}}}};return EventBus;});
事件订阅代码:_
EventBus.on("someEvent", function(event) {// 这里处理事件var c = event.data.a + event.data.b;});
事件发布代码:
EventBus.fire({type: "someEvent",data: {aaa: 1,bbb: 2}});
