AngularJS实例教程(一)——数据绑定与监控
AngularJS实例教程(二)——作用域与事件

数据绑定与监控

$watch

  1. $scope.a = 1;
  2. $scope.$watch("a", function(newValue, oldValue) {
  3. alert(oldValue + " -> " + newValue);
  4. });
  5. $scope.changeA = function() {
  6. $scope.a++;
  7. };
  8. 以上这种方式可以监控到最直接的赋值,包括各种基本类型,以及复杂类型的引用赋值,
  9. 比如说下面这个数组被重新赋值了,就可以被监控到:
  10. ***************************************************************
  11. $scope.arr = [0];
  12. $scope.$watch("arr", function(newValue) {
  13. alert("change:" + newValue.join(","));
  14. });
  15. $scope.changeArr = function() {
  16. $scope.arr = [7, 8];
  17. };
  18. 但这种监控方式只能处理引用相等的判断,对于一些更复杂的监控,需要更细致的处理。
  19. 比如说,我们有可能需要监控一个数组,但并非监控它的整体赋值,而是监控其元素的变更:
  20. $scope.$watch("arr", function(newValue) {
  21. alert("deep:" + newValue.join(","));
  22. }, true);
  23. $scope.addItem = function() {
  24. $scope.arr.push($scope.arr.length);
  25. };
  26. **注意$watch的第三个参数true**

对某个选中的元素进行进一步处理

  1. 不知道大家有没有遇到过这样的场景,有一个数据列表,点中其中某条,这条就改变样式变成加亮,
  2. 如果用传统的方式,可能要添加一些事件,然后在其中作一些处理,但使用数据绑定,能够大幅简化代码:
  3. function ListCtrl($scope) {
  4. $scope.items = [];
  5. for (var i=0; i<10; i++) {
  6. $scope.items.push({
  7. title:i
  8. });
  9. }
  10. $scope.selectedItem = $scope.items[0];
  11. $scope.select = function(item) {
  12. $scope.selectedItem = item;
  13. };
  14. }
  15. <ul class="list-group" ng-controller="ListCtrl">
  16. <li ng-repeat="item in items" ng-class="{true:'list-group-item active', false: 'list-group-item'}[item==selectedItem]" ng-click="select(item)">
  17. {{item.title}}
  18. </li>
  19. </ul>

作用域与事件

作用域的继承关系

  1. 在开发过程中,我们可能会出现控制器的嵌套,看下面这段代码:
  2. <div ng-controller="OuterCtrl">
  3. <span>{{a}}</span>
  4. <div ng-controller="InnerCtrl">
  5. <span>{{a}}</span>
  6. </div>
  7. </div>
  8. function OuterCtrl($scope) {
  9. $scope.a = 1;
  10. }
  11. function InnerCtrl($scope) {
  12. }
  13. 注意结果,我们可以看到界面显示了两个1,而我们只在OuterCtrl的作用域里定义了a变量,
  14. 但界面给我们的结果是,两个a都有值。这里内层的a值显然来自外层,
  15. 因为当我们对界面作出这样的调整之后,就只有一个了:
  16. <div ng-controller="OuterCtrl">
  17. <span>{{a}}</span>
  18. </div>
  19. <div ng-controller="InnerCtrl">
  20. <span>{{a}}</span>
  21. </div>
  22. 这是为什么呢?在Angular中,如果两个控制器所对应的视图存在上下级关系,
  23. 它们的作用域就自动产生继承关系。什么意思呢?
  24. 先考虑在纯JavaScript代码中,两个构造函数各自有一个实例:
  25. function Outer() {
  26. this.a = 1;
  27. }
  28. function Inner() {
  29. }
  30. var outer = new Outer();
  31. var inner = new Inner();
  32. 在这里面添加什么代码,能够让inner.a == 1呢?
  33. 熟悉JavaScript原型的我们,当然毫不犹豫就加了一句:Inner.prototype = outer;
  34. function Outer() {
  35. this.a = 1;
  36. }
  37. function Inner() {
  38. }
  39. var outer = new Outer();
  40. Inner.prototype = outer;
  41. var inner = new Inner();
  42. 于是就得到想要的结果了。
  43. 再回到我们的例子里,Angular的实现机制其实也就是把这两个控制器中的$scope作了关联,
  44. 外层的作用域实例成为了内层作用域的原型。
  45. 以此类推,整个Angular应用的作用域,都存在自顶向下的继承关系,最顶层的是$rootScope
  46. 然后一级一级,沿着不同的控制器往下,形成了一棵作用域的树,
  47. 这也就像封建社会:天子高高在上,分茅裂土,公侯伯子男,一级一级往下,层层从属。

简单变量的取值与赋值

  1. 既然作用域是通过原型来继承的,自然也就可以推论出一些特征来。比如说这段代码,点击按钮的结果是什么?
  2. <div ng-controller="OuterCtrl">
  3. <span>{{a}}</span>
  4. <div ng-controller="InnerCtrl">
  5. <span>{{a}}</span>
  6. <button ng-click="a=a+1">a++</button>
  7. </div>
  8. </div>
  9. function OuterCtrl($scope) {
  10. $scope.a = 1;
  11. }
  12. function InnerCtrl($scope) {
  13. }
  14. 点了按钮之后,两个a不一致了,里面的变了,外面的没变,这是为什么?
  15. 原先两层不是共用一个a吗,怎么会出现两个不同的值?
  16. 看这句就能明白了,相当于我们之前那个例子里,这样赋值了:
  17. function Outer() {
  18. this.a = 1;
  19. }
  20. function Inner() {
  21. }
  22. var outer = new Outer();
  23. Inner.prototype = outer;
  24. var inner = new Inner();
  25. inner.a = inner.a + 1;
  26. 最后这句,很有意思,它有两个过程,取值的时候,因为inner自身上面没有,所以沿着原型往上取到了1
  27. 然后自增了之后,赋值给自己,这个赋值的时候就不同了,
  28. 敬爱的林副主席教导我们:有a就赋值,没有a,创造一个a也要赋值。
  29. (之前是原型里有a,加完一次之后自己有一个值为2a。同时原型里还有个值为1a
  30. 所以这么一来,inner上面就被赋值了一个新的aouter里面的仍然保持原样,这也就导致了刚才看到的结果。
  31. 初学者在这个问题上很容易犯错,如果不能随时很明确地认识到这些变量的差异,很容易写出有问题的程序。
  32. 既然这样,我们可以用一些别的方式来减少变量的歧义。

对象在上下级作用域之间的共享

  1. 比如说,我们就是想上下级共享变量,不创建新的,该怎么办呢?
  2. 考虑下面这个例子:
  3. function Outer() {
  4. this.data = {
  5. a: 1
  6. };
  7. }
  8. function Inner() {
  9. }
  10. var outer = new Outer();
  11. Inner.prototype = outer;
  12. var inner = new Inner();
  13. console.log(outer.data.a);
  14. console.log(inner.data.a);
  15. // 注意,这个时候会怎样?
  16. inner.data.a += 1;
  17. console.log(outer.data.a);
  18. console.log(inner.data.a);
  19. 这次的结果就跟上次不同了,原因是什么呢?因为两者的data是同一个引用,对这个对象上面的属性修改,是可以反映到两级对象上的。我们通过引入一个data对象的方式,继续使用了原先的变量。把这个代码移植到AngularJS里,就变成了下面这样:
  20. <div ng-controller="OuterCtrl">
  21. <span>{{data.a}}</span>
  22. <div ng-controller="InnerCtrl">
  23. <span>{{data.a}}</span>
  24. <button ng-click="data.a=data.a+1">increase a</button>
  25. </div>
  26. </div>
  27. function OuterCtrl($scope) {
  28. $scope.data = {
  29. a: 1
  30. };
  31. }
  32. function InnerCtrl($scope) {
  33. }
  34. 从这个例子我们就发现了,如果想要避免变量歧义,显式指定所要使用的变量会是比较好的方式,那么如果我们确实就是要在上下级分别存在相同的变量该怎么办呢,比如说下级的点击,想要给上级的a增加1,我们可以使用$parent来指定上级作用域。
  35. <div ng-controller="OuterCtrl">
  36. <span>{{a}}</span>
  37. <div ng-controller="InnerCtrl">
  38. <span>{{a}}</span>
  39. <button ng-click="$parent.a=a+1">increase a</button>
  40. </div>
  41. </div>
  42. function OuterCtrl($scope) {
  43. $scope.a = 1;
  44. }
  45. function InnerCtrl($scope) {
  46. }

不请自来的新作用域(ng-repeat)

  1. 在一个应用中,最常见的会创建作用域的指令是ng-controller,因为它会实例化一个新的控制器,
  2. 往里面注入一个$scope,也就是一个新的作用域,所以一般人都会很自然地理解这里面的作用域隔离关系。
  3. 但是对于另外一些情况,就有些困惑了,比如说,ng-repeat,怎么理解这个东西也会创建新作用域呢?
  4. 还是看之前的例子:
  5. $scope.arr = [1, 2, 3];
  6. <ul>
  7. <li ng-repeat="item in arr track by $index">{{item}}</li>
  8. </ul>
  9. ng-repeat的表达式里,有一个item,我们来思考一下,这个item是个什么情况。
  10. 在这里,数组中有三个元素,在循环的时候,这三个元素都叫做item,这时候就有个问题,
  11. 如何区分每个不同的item,可能我们这个例子还不够直接,那改一下:
  12. <div>outer: {{sum1}}</div>
  13. <ul>
  14. <li ng-repeat="item in arr track by $index">
  15. {{item}}
  16. <button ng-click="sum1=sum1+item">increase</button>
  17. <div>inner: {{sum1}}</div>
  18. </li>
  19. </ul>
  20. 这个例子运行一下,我们会发现每个item都会独立改变,说明它们确实是区分开了的。
  21. 事实上,Angular在这里为ng-repeat的每个子项都创建了单独的作用域,
  22. 所以,每个item都存在于自己的作用域里,互不影响。
  23. 有时候,我们是需要在循环内部访问外层变量的,回忆一下,在本章的前面部分中,我们举例说,
  24. 如果两个控制器,它们的视图有包含关系,内层控制器的作用域可以通过$parent来访问外层控制器作用域上的变量,
  25. 那么,在这种循环里,是不是也可以如此呢?
  26. **疑惑,上面例子sum1未定义可以正常按0开始计算,且scope.sum1 = undefined后也能正常执行表达式**
  27. 看这个例子:
  28. <div>outer: {{sum2}}</div>
  29. <ul>
  30. <li ng-repeat="item in arr track by $index">
  31. {{item}}
  32. <button ng-click="$parent.sum2=sum2+item">increase</button>
  33. <div>inner: {{sum2}}</div>
  34. </li>
  35. </ul>
  36. 果然是可以的。很多时候,人们会把$parent误认为是上下两级控制器之间的访问通道,
  37. 但从这个例子我们可以看到,并非如此,只是**两级作用域**而已,作用域跟控制器还是不同的,
  38. 刚才的循环可以说是有两级作用域,但都处于同一个控制器之中。
  39. 刚才我们已经提到了ng-controllerng-repeat这两个常用的内置指令,两者都会创建新的作用域,
  40. 除此之外,还有一些其他指令也会创建新的作用域,很多初学者在使用过程中很容易产生困扰。
  41. 第一章我们提到用ng-showng-hide来控制某个界面块的整体展示和隐藏,但同样的功能其实也可以用ng-if来实现。
  42. 那么这两者的差异是什么呢,所谓showhide,大家很好理解,就是某个东西原先有,只是控制是否显式,
  43. if的含义是,如果满足条件,就创建这块DOM,否则不创建。
  44. 所以,ng-if所控制的界面块,只有条件为真的时候才会存在于DOM树中。
  45. 除此之外,两者还有个差异,ng-showng-hide是不自带作用域的,而ng-if则自己创建了一级作用域。
  46. 在用的时候,两者就是有差别的,比如说内部元素访问外层定义的变量,
  47. 就需要使用类似ng-repeat那样的$parent语法了。
  48. 相似的类型还有ng-switchng-include等等,规律可以总结,
  49. 也就是那些会动态创建一块界面的东西,都是自带一级作用域。


上面第一个例子的运行结果(点击两次后结果如图,第一次点击时分别是1,2,3)
图片.png
__上面第二个例子的运行结果(所有sum2一起变化)

图片.png

“悬空”的作用域

在任意一个已有的作用域上调用$new(),就能创建一个新的作用域:
_

  1. var newScope = scope.$new();


newScope跟任何界面模板都不存在绑定关系,创建它的作用域会成为它的$parent。
这种作用域可以经过$compile阶段,与某视图模板进行融合。

理解**:**我们可以用DocumentFragment作类比,当作用域被创建的时候,就好比是创建了一个DocumentFragment,它是不在DOM树上的,只有当它被append到DOM树上,才能够被当做普通的DOM来使用。

如果我们想要监控一个数据的变化,但这个数据并非绑定到界面上的,比如下面这样,怎么办?
_

  1. function IsolateCtrl($scope) {
  2. var child = {
  3. a: 1
  4. };
  5. child.a++;
  6. }

这里的child,它并未绑定到$scope上,如果我们想要在a变化的时候做某些事情,是没有办法做的。

注意:我们的$watch和$eval之类的方法,其实都是实现在作用域对象上的,即使作用域对象没有与界面产生关联,也依旧可以使用这些方法。

  1. function IsolateCtrl($scope) {
  2. var child = $scope.$new();
  3. child.a = 1;
  4. child.$watch("a", function(newValue) {
  5. alert(newValue);
  6. });
  7. $scope.change = function() {
  8. child.a++;
  9. };
  10. }

这时候child里面a的变更就可以被观测到,并且,这个child只有本作用域可以访问到。

作用域上的事件($emit,$broadcast)

event_flat.png
如果子视图A1想要发出一个业务事件,使得B1和B2能够得到通知,过程就会是:

  • 沿着父作用域一路往上到达双方共同的祖先作用域
  • 从祖先作用域一级一级往下进行广播,直到到达需要的地方event.png ```javascript 从作用域往上发送事件,使用scope.$emit $scope.$emit(“someEvent”, {});

从作用域往下发送事件,使用scope.$broadcast $scope.$broadcast(“someEvent”, {});

第二个参数是要随事件带出的数据

  1. <a name="pzSXw"></a>
  2. ### 事件的接收与阻止($on)
  3. _<br />_**无论是$emit还是$broadcast发送的事件,都可以被接收,接收这两种事件的方式是一样的:**_<br />_
  4. ```javascript
  5. $scope.$on("someEvent", function(e) {
  6. // 这里从e上可以取到发送过来的数据
  7. });

注意:被接收不代表传播终止,依旧保持状态:

  • $emit的事件将继续向上传播
  • $broadcast的事件将继续向下传播

注意:有时候想让事件停下来,把事件中止,但是

  • $emit发出的事件是可以被中止的
  • $broadcast发出的不可以被中止

阻止$emit的传播可以用:

  1. $scope.$on("someEvent", function(e) {
  2. e.stopPropagation();
  3. });

事件总线

当层级较多时使用$emit和$broadcast效率会较低,因此我们能不能这样:搞一个专门负责通讯的机构,大家的消息都发给它,然后由它发给相关人员,其他人员在理念上都是平级关系。
ng_event_bus.png

使用一个公共模块:
_

  1. app.factory("EventBus", function() {
  2. var eventMap = {};
  3. var EventBus = {
  4. on : function(eventType, handler) {
  5. //multiple event listener
  6. if (!eventMap[eventType]) {
  7. eventMap[eventType] = [];
  8. }
  9. eventMap[eventType].push(handler);
  10. },
  11. off : function(eventType, handler) {
  12. for (var i = 0; i < eventMap[eventType].length; i++) {
  13. if (eventMap[eventType][i] === handler) {
  14. eventMap[eventType].splice(i, 1);
  15. break;
  16. }
  17. }
  18. },
  19. fire : function(event) {
  20. var eventType = event.type;
  21. if (eventMap && eventMap[eventType]) {
  22. for (var i = 0; i < eventMap[eventType].length; i++) {
  23. eventMap[eventType][i](event);
  24. }
  25. }
  26. }
  27. };
  28. return EventBus;
  29. });


事件订阅代码:_

  1. EventBus.on("someEvent", function(event) {
  2. // 这里处理事件
  3. var c = event.data.a + event.data.b;
  4. });

事件发布代码:

  1. EventBus.fire({
  2. type: "someEvent",
  3. data: {
  4. aaa: 1,
  5. bbb: 2
  6. }
  7. });