AngularJS入门与进阶
上QQ阅读APP看书,第一时间看更新

5.3 作用域高级特性

前面介绍了AngularJS作用域的基本概念以及作用域原型继承机制,本节将介绍AngularJS作用域的一些高级特性,包括作用域属性监视、digest循环等。

5.3.1 $watch方法监视作用域

在使用AngulaJS框架编写应用时,我们经常需要做的一件事情就是对作用域中的属性进行监视,并对其发生的变化做出相应的处理。AngularJS为我们提供了一个非常方便的$watch()方法,可以帮助我们监视作用域中属性的变化。下面给出一个使用$watch()方法监视作用域属性变化的案例:

代码清单:ch05\ch05_05.html

        <!doctype html>
        <html ng-app="watchModule">
        <head>
            <meta charset="UTF-8">
            <title>ch05_05</title>
            <script type="text/javascript" src="../angular-1.5.5/angular.js">
            </script>
        </head>
        <body>
            <input ng-model='name' type='text'/>
            <div>change count: {{count}}</div>
            <script>
                angular.module('watchModule', [])
                      .run(['$rootScope', function($rootScope){
                            $rootScope.count = 0;
                            $rootScope.name = ’江荣波’;
                            $rootScope.$watch('name', function(){
                                  $rootScope.count++;
                            })
                      }]);
            </script>
        </body>
        </html>

这段代码非常简单,使用作用域对象的$watch()方法对$rootScope中的name属性进行监视,并在它发生变化的时候将$rootScope中的count属性值增加1。因此,每当我们对输入框中的内容进行一次修改时,界面显示的change count数值就会增加1。

在AngularJS内部,每当我们对ng-model绑定的name属性进行一次修改时,AngularJS内部的$digest循环就会运行一次,并在运行结束之后检查我们使用$watch()方法来监视的内容,如果和上一次进行$digest之前相比有了变化,就执行我们使用$watch()方法绑定的处理函数。

然而,我们在实际运用中常常不只是对一个原始类型的属性进行监视,如果读者还记得JavaScript语言中的6种数据类型,就一定会记得基本类型(数字、字符串)和引用类型的区别。对于基本类型,如果我们使用了一个赋值操作,这个基本类型变量就会“真正”被复制一次,然而对于引用类型,在进行赋值时,仅仅是将赋值的变量指向了这个引用类型。在AngularJS作用域对象的$watch()方法中,对基本类型和引用类型的操作有所不同。基本类型,就像我们上面的例子,没有什么特别之处,然而要对一个引用类型进行监视时,尤其是在实际运用中常见的数组对象,情况就不一样了。我们来看下面的例子:

代码清单:ch05\ch05_06.html

        <! doctype html>
        <html ng-app="watchModule">
        <head>
            <meta charset="UTF-8">
            <title>ch05_06</title>
            <script type="text/javascript" src="../angular-1.5.5/angular.js">
            </script>
        </head>
        <body>
            <div ng-repeat='item in items'>
                <input ng-model='item.value'/>&nbsp; <span>{{item.value}}</
                span><br/><br/>
            </div>
            <div>change count: {{count}}</div>
            <script>
                angular.module('watchModule', [])
                      .run(['$rootScope', function($rootScope){
                            $rootScope.count = 0;
                              $rootScope.items = [
                                  { "value": 1 },
                                  { "value": 2 },
                                  { "value": 3 },
                                  { "value": 4 }
                            ]
                            $rootScope.$watch('items', function(){
                                  $rootScope.count++;
                            })
                      }]);
            </script>
        </body>
        </html>

在浏览器中运行ch05_06.html页面,如图5.1所示。

图5.1 作用域监视案例

此时读者如果对上面4个文本框中内容进行修改,就会发现count值始终不变,这是怎么回事?难到$watch方法不起作用了吗?

正如前面提到的,$watch()方法在对待基本类型和引用类型时会有不同的处理方式,这需要首先介绍一下$watch()方法的第三个参数。在前面的例子中,我们知道,$watch()方法可以接收两个参数,第一个参数是需要监视的属性,第二个参数是在监视属性发生变化时需要回调的方法,实际上$watch()方法还能接收第三个参数,默认情况下参数值为false。

在默认情况下,即不显式指明第三个参数或者将其指明为false时,我们进行的监视叫作“引用监视”(reference watch),也就是说只要监视的对象引用没有发生变化,就不算它发生变化。

具体来说,在上面的例子中,只要是items数组的引用没有发生变化,就算items数组中的一些元素属性发生了变化,$watch()方法也不会执行回调方法。那么在什么时候算是引用发生了变化呢?比如说将一个新的数组newItems赋值给items,此时$watch才会认为监视的属性发生了变化,进而调用回调方法。

相反,将$watch()方法的第三个变量设置为true,此时进行的监视就叫作“全等监视”(equality watch)。此时只要监视的属性发生变化,$watch就会执行相应的回调方法。

读者可参考ch05_07.html案例,在$watch方法增加第三个参数值并设为true,在浏览器中预览就会发现只要任意一个文本框内容发生变化,count的值就会增加。

既然全等监视这么好,那么我们为什么不直接用全等监视呢?当然,任何事情都有好坏两个方面,全等监视固然好,但是在运行时需要先遍历整个监视对象,然后在每次$digest之前使用angular.copy()将整个对象深复制一遍,然后在运行之后用angular.equal()将前后的对象进行对比。上面例子中的items比较简单,因此可能在性能上不会有什么差别,但是到了实际生产环境时,我们要面对的数据千千万万,可能因为全等监视这一个设置就消耗大量的资源,让应用停滞不前。因此需要在使用时进行权衡,再决定究竟使用哪一种监视方式。

除了上面提到的两种方式之外,在angular1.1.4版本之后,新增加了一个$watchCollection()方法来针对数组(也就是集合)进行监视,它的性能介于全等监视和引用监视之间,即它并不会对数组中每一项的属性进行监视,但是可以对数组的项目增减做出反应。还是上面的例子,我们稍做修改,完整代码可参考ch05_08.html:

        angular.module('watchModule', [])

        .run(['$rootScope', function($rootScope){

            $rootScope.count = 0;

            $rootScope.items = [

            { "value": 1 },

            { "value": 2 },

            { "value": 3 },

            { "value": 4 }

            ]

            $rootScope.$watchCollection('items', function(){

            $rootScope.count++;

            })

       }]);

如上面的代码所示,把$watch()方法修改为$watchCollection()方法,如果改变了items[0]的value属性值,$watch()方法并不会做出反应,但是如果我们在items上push或者pop一个元素,$watch()方法就会执行回调方法了。

5.3.2 作用域监视解除

上一小节我们学习了AngularJS作用域监视机制,本小节介绍如何解除作用域监视。这需要我们关注一下$watch()方法的返回值,该方法调用完毕后返回另一个方法,我们只需调用返回的方法即可解除作用域监视。我们来看下面的例子:

代码清单:ch05\ch05_09.html

        <!doctype html>
        <html ng-app=”watchModule”>
        <head>
            <meta charset=”UTF-8”>
            <title>ch05_09</title>
            <script type=”text/javascript” src=”../angular-1.5.5/angular.js”>
            </script>
        </head>
        <body>
            <input ng-model='num' type='number'/>
            <div>change count: {{count}}</div>
            <script>
                angular.module('watchModule', [])
                      .run(['$rootScope', function($rootScope){
                            $rootScope.count = 0;
                            $rootScope.num = 100;
                            var unbindWatcher= $rootScope
                            .$watch('num', function(newValue, oldVaule){
                                if(newValue == 2){
                                    unbindWatcher();
                                }
                                $rootScope.count++;
                            })
                      }]);
            </script>
        </body>
        </html>

在浏览器中运行ch05_09.html页面,效果如图5.2所示。

图5.2 作用域监视解除案例

在本案例中,最初文本框内容发生改变时count值会累加,当文本框值为2时,我们调用$watch()返回的方法unbindWatcher()解除了作用域监视,所以再次修改本文框内容时count值不再累加。

5.3.3 $apply方法与$digest循环

$apply与$digest是AngularJS中两个核心的概念,如果读者想深入研究AngularJS双向数据绑定实现原理,就必须先了解这两个概念,为了方便说明问题,我们先看下面的代码片段:

        <input  id="input" type="text" ng-model="name"/>
        <div id="output">{{name}}</div>

当我们写下AngularJS表达式(例如{{name}})时,AngularJS框架会在幕后为我们在$scope中设置一个watcher,用来在数据发生变化的时候更新View。这里的watcher和5.3.2小节中手动调用$watch方法添加的watcher是一样的:

        $scope.$watch('name', function(newValue, oldValue){
          //update the DOM with newValue
        });

如上面的代码所示,$watch()方法的第二个参数是一个回调方法,该方法会在name属性的值发生变化的时候被调用。当name发生变化的时候,这个回调方法会被调用来更新View,这点不难理解,但是,还存在一个很重要的问题!AngularJS是如何知道什么时候要调用这个回调方法的呢?换句话说,AngularJS是如何知晓name属性发生了变化才调用了对应的回调方法呢?它会周期性地运行一个函数来检查scope模型中的数据是否发生了变化,这就是所谓的$digest循环。

在$digest循环中,watchers会被触发。当一个watcher被触发时,AngularJS会检测scope模型,如果它发生了变化,那么关联到该watcher的回调方法就会被调用。那么,$digest循环是在什么时候以各种方式开始的呢?

在调用了$scope.$digest()后,$digest循环就开始了。假设你在一个ng-click指令对应的事件处理方法中更改了scope中的一条数据,此时AngularJS会自动地通过调用$digest()来触发一轮$digest循环。当$digest循环开始后,它会触发每个watcher。这些watcher会检查scope中的当前属性值是否和上一次$digest循环时的属性值相同。如果不同,对应的回调方法就会被执行。调用该方法的结果就是View中的表达式内容被更新。除了ng-click指令,还有一些其他的AngularJS内置指令和服务来让我们能够更改模型数据(比如ng-model指令、$timeout服务等)和自动触发一次$digest循环。

除此之外,还有一个问题,在上面的例子中,AngularJS并不直接调用$digest()方法,而是调用$scope.$apply(),后者会调用$rootScope.$digest()。因此,一轮$digest循环在$rootScope开始,随后会访问所有子作用域中的watcher。

5.3.4 $apply与$digest应用实战

当AngularJS作用域中的模型数据发生变化时,AngularJS会自动触发$digest循环,从而达到自动更新视图的目的。但是在有些情况下,模型数据修改后需要我们手动调用$apply()方法来触发$digest循环,例如使用JavaScript的setTimeout()方法来更新一个模型数据,AngularJS框架就没有办法知道我们修改了什么,也就无法触发$digest循环了。下面的案例就能说明这种情况:

代码清单:ch05\ch05_10.html

        <!doctype html>
        <html ng-app=”msgModule”>
        <head>
            <meta charset=”UTF-8”>
            <title>ch05_10</title>
            <script type=”text/javascript” src=”../angular-1.5.5/angular.js”>
            </script>
        </head>
        <body>
            <div ng-controller=”MsgController”>
                <div>
                    <button ng-click=”scheduleTask()">3秒后回显信息
                    </button>
                </div>
                <div>
                    {{message}}
                </div>
            </div>
            <script>
                angular.module('msgModule', []).controller('MsgController',
                    function($scope){
                        $scope.scheduleTask = function() {
                            setTimeout(function() {
                                $scope.message = ’信息内容’;
                                console.log('message='+$scope.message);
                            }, 3000);
                        }
                    });
            </script>
        </body>
        </html>

如上面的代码所示,我们使用ng-click指令对按钮单击事件进行事件绑定,单击按钮时会调用scheduleTask()方法,在该方法中使用setTimeout()方法3s后设置message属性的内容。在浏览器中预览ch05_10.html页面,效果如图5.3所示。

图5.3 $digest循环无法触发案例

单击按钮3s后,控制台中输出信息,说明我们在作用域中添加message属性成功,但是页面中并没有回显任何内容,这种情况下AngularJS框架无法自动触发$digest循环,需要我们手动调用$apply()方法来触发$digest循环。$apply()方法接收一个方法作为参数。我们对上面的控制器代码进行修改,下面只列出关键代码片段,完整代码可参考ch05\ch05_11.html。

        angular.module('msgModule', []).controller('MsgController',
            function($scope){
            $scope.scheduleTask = function() {
            setTimeout(function() {
            $scope.$apply(function(){
            $scope.message = ’信息内容’;
            console.log('message='+$scope.message);
            });
            }, 3000);
            }
            });

如上面的黑体代码所示,我们把修改作用域属性的代码移到一个匿名方法中,然后把该匿名方法作为$apply()方法的参数,这样就可以触发$digest循环了。在浏览器中预览ch05_11. html,效果如图5.4所示。

图5.4 调用$apply方法触发$digest实例

单击按钮,3秒过后,作用域中message属性内容已经能够回显到页面中,说明我们手动调用$apply()方法触发$digest循环成功。

5.3.5 $timeout与$interval服务介绍

在5.3.4小节中我们使用JavaScript的setTimeout()方法达到延迟执行某个方法的效果。JavaScript中还有一个与setTimeout()类似的方法setInterval(),作用是每隔一段时间调用一次特定的JavaScript方法。这两个方法都需要我们手动调用$apply()方法来触发$digest循环。

使用AngularJS内置的指令或服务通常不需要我们手动调用$apply()方法触发$digest循环,AngularJS为我们提供了两个实用的服务$timeout和$interval,功能和setTimeout()、setInterval()方法相同,使用这两个服务修改作用域属性时会自动触发$digest循环,我们可以对5.3.4小节的案例进行修改,使用$timeout服务实现,控制器代码如下,完整代码请参考ch05\ch05_12.html。

        angular.module('msgModule', []).controller('MsgController',
            function($scope, $timeout){
            $scope.scheduleTask = function() {
            $timeout(function() {
            $scope.message = ’信息内容’;
            console.log('message='+$scope.message);
            }, 3000);
            }
            });

在浏览器中预览ch05_12.html,效果和5.3.4小节相同,$interval的使用方法和$timeout类似,这里不再赘述。