官术网_书友最值得收藏!

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類似,這里不再贅述。

主站蜘蛛池模板: 错那县| 蓝田县| 南皮县| 和平区| 德钦县| 赤城县| 石柱| 天气| 新巴尔虎左旗| 东安县| 稻城县| 彭泽县| 合川市| 芜湖县| 恩平市| 绿春县| 绥江县| 巧家县| 阿拉善右旗| 三穗县| 靖远县| 霞浦县| 唐海县| 砚山县| 金秀| 大连市| 开平市| 台南市| 海原县| 威海市| 东乌珠穆沁旗| 乌鲁木齐市| 馆陶县| 太仆寺旗| 秭归县| 积石山| 兴宁市| 上杭县| 商洛市| 乡宁县| 连云港市|