- AngularJS入門與進階
- 江榮波
- 3658字
- 2020-11-28 23:44:33
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'/> <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類似,這里不再贅述。
- Learning LibGDX Game Development(Second Edition)
- 嵌入式軟件系統測試:基于形式化方法的自動化測試解決方案
- Kali Linux Wireless Penetration Testing Beginner's Guide(Third Edition)
- Modern JavaScript Applications
- Terraform:多云、混合云環境下實現基礎設施即代碼(第2版)
- 編程菜鳥學Python數據分析
- C#開發案例精粹
- 開源項目成功之道
- 微信小程序開發實戰:設計·運營·變現(圖解案例版)
- Learning JavaScript Data Structures and Algorithms(Second Edition)
- 分布式數據庫原理、架構與實踐
- RESTful Web Clients:基于超媒體的可復用客戶端
- 現代CPU性能分析與優化
- Python編程基礎教程
- Data Manipulation with R(Second Edition)