在標準的瀏覽器流程中,當事件被觸發時,瀏覽器會執行注冊給該事件的回調函數。頁面加載、$http
請求返回響應、鼠標移動以及按鈕被點擊等情況都會觸發事件。
當事件被觸發時,JS就會創建一個事件對象,并執行這個事件對象所在的監聽特定事件的所有函數。然后它會運行JS函數內的回調方法,這會回到瀏覽器中,還可能更新DOM。
同一時間不能運行兩個事件。瀏覽器會等待前一個事件處理程序執行完成,再調用下一個事件處理程序。
在非Angular JS環境中,可以給div
的點擊事件附加一個回調函數。只要發現元素上的點擊事件,這個回調函數就會運行。
var div = document.getElementById("clickDiv");
div.addEventListener("click", function(evt) {
console.log("evt", evt);
});
無論何時,只要瀏覽器檢測到點擊事件,就會調用使用addEventListener
注冊到文檔上的函數。
當我們將Angular混入這個流程中時,它會擴展這個標準的瀏覽器流程,創建一個Angular上下文。這個Angular上下文指的是運行在Angular事件循環內的特定代碼,該Angular事件循環通常被稱作$digest
循環。$digest
循環有兩個主要組成部分:
-
$watch
列表 -
$evalAsync
列表
$watch列表
每當我們在視圖中追蹤一個事件時,會給它注冊一個回調函數,然后希望在觸發該事件時調用這個回調函數。
<input ng-model="name" type="text" placeholder="Your name">
<h1>Hello {{ name }}</h1>
無論何時,只要用戶更新這個輸入字段,{{name}}
就會改變。發生這一變化是因為我們把輸入字段綁定給了$scope.name
屬性。為了更新這個視圖,Angular需要追蹤變化。它是通過給$watch
列表添加一個監控函數做到這一點的。
$scope
對象上的屬性只會在其被用于視圖時綁定。對于所有綁定給同一$scope
對象的UI元素,只會添加一個$watch
到$watch
列表中。這些$watch
列表會在$digest
循環中通過一個叫做“臟值檢查”的程序解析。
臟值檢查
臟值檢查可歸結為一個非?;A的概念:檢查值是否發生了變化,而整個應用還沒同步該變化。
Angular應用持續跟蹤當前監控的值。Angular會遍歷$watch
列表,如果從舊值更新后的值沒有發生變化,它會繼續遍歷監控列表。如果值發生了變化,該應用會啟用新值并繼續遍歷$watch
列表。
Anguar遍歷完整個$watch
列表,只要有任何值發生變化,應用將會退回到$watch
循環中,直到檢測到不再有任何變化。
為什么要再次運行這一循環?因為如果你更新了$watch
列表中某個用于更新另一個值的值,Angular將檢測不到更新,除非再次運行這個循環。
如果這個循環運行10次或者更多次,Angular應用會拋出一個異常,同時停止運行。如果Angular沒有拋出這個異常,應用就可能進入無限循環。
$watch
$scope
對象上的$watch
方法會給Angular事件循環內的每個$digest
調用裝配一個臟值檢查。如果在表達式上檢測到變化,Angular總是會返回$digest
循環。
$watch
函數本身接受兩個必要參數和一個可選的參數:
- watchExpression
watchExpression
可以是一個作用域對象的屬性,或者是一個函數。在$digest
循環中的每個$digest
調用都會涉及它。
如果watchExpression
是一個字符串,Angular會在$scope
上下文中對它求值。如果它是一個函數,那么Angular會認為它會返回應該被監控的值。 - listener/callback
作為回調的監聽器函數,它只會在watchExpression
的當前值與先前值不相等(除了首次運行初始化期間)時調用。 - objectEquality(可選)
objectEquality
是一個進行比較的布爾值,用來告訴Angular是否檢查嚴格相等。
$watch
函數會給監聽器返回一個注銷函數,我們可以調用這個注銷函數來取消Angular對當前值的監控。
//...
var unregisterWatch=$scope.$watch('newUser.email',function(newVal,oldVal){
if (newVal === oldVal) return; // 初始化
});
// 稍后,可以通過調用這個注銷函數來注銷這個監控器
unregisterWatch();
假如完成了對newUser.email
的監控,那么可以通過調用它所返回的注銷函數來清除這個監控器。
例如,你想要解析一個輸入字段的值,然后使用空格分割全名的方式找到名字和姓氏。假定給定的視圖看起來像這樣:
<input type="text" ng-model="full_name" placeholder="Enter your full name"/>
我們在full_name
屬性上設置一個$watch
監聽器來檢測值的任意變化。
angular.module("myApp").controller("MyController",['$scope',function($scope){
$scope.$watch('full_name', function(newVal, oldVal, scope) {
// newVal表示在這里可以用的full_name新值
// 而oldVal表示full_name的舊值
});
}]);
監聽函數會在初始化時被調用一次,而此時newVal
和oldVal
的值都是undefined
(并且是相等的)。在這種情況下,如果正處在初始化階段或者先前的值發生了變化,通常最好是檢查內部的表達式。在監控函數內很容易實現這一檢查。
$scope.$watch('full_name',function(newVal,oldVal,scope) {
if(newVal === oldVal) {
// 只會在監控器初始化階段運行
} else {
// 初始化之后發生的變化
}
});
在這段代碼中,$scope.$watch()
函數在$scope
對象上為full_name
屬性設置了一個監控表達式。
$watchCollection
此外,Angular還允許我們為對象的屬性或者數組的元素設置淺監控,然后只要屬性發生變化就觸發監聽器回調。
使用$watchCollection
還可以檢測對象或數組何時發生了變化,以便確定對象或數組中的條目是何時添加、移除或者移動的。$watchConllection
的行為與$digest
循環中標準的$watch
的行為一樣,我們甚至可以把它當作標準的$watch
。
$watchCollectiion()
函數接受2個參數。
- obj(字符串/函數)
這個對象就是一個要監控的對象。如果傳入一個字符串,它將被當作Angular表達式求值。
如果傳入的是一個函數,將在當前作用域中被調用,并且會返回要監控的值。 - listener(函數)
這個回調函數會在集合發生變化時觸發。類似于$watch函數,這個函數會被來自$watch
的新集合觸發調用,而原來的集合(先前集合的副本)以及所在的作用域也隨之生效。
$watchConllection()
函數也返回一個注銷函數。調用這個注銷函數時,也會取消集合上的$watch
。
$scope.$watchCollection('names',function(newNames,oldNames,scope) {
// names集合已經發生了變化
});
頁面中的$digest循環
首先,假設有一個登錄頁面,這個頁面帶有一個唯一的用戶名字段,允許用戶使用唯一的表單驗證進行登錄。
<h2>Sign in</h2>
<input type="text" placeholder="Your name" ng-model="name" ng-minlength="3" />
<input type="submit" ng-click="login()" value="Login" />
這里通過ng-model
指令在視圖中綁定了一個name
,Angular會設置一個隱式的監控器,將這個輸入字段的值綁定為當前的$scope
對象。當用戶輸入一個字符到表單中時,Angular上下文就會生效并開始遍歷$$watchers
($watch
列表)。
在這個例子中,$watch
列表只包含了一個唯一的元素:$scope.name
。由于用戶通過輸入一個字符改變了輸入字段的值,這個監控函數就會在$scope.name
綁定上執行。在我們退出$digest
循環之前,這一行為會觸發在該值(由ng-model
綁定)上運行的驗證和格式化操作。
由于在digest
循環中值發生了變化,Angular需要再次運行這一循環以確定它沒有改變作用域對象上的其他值。
為什么要再次運行digest
循環?如果有一個名為$scope.full_name
的屬性由$scope.first_name+$scope.last_name
組成,那么這些值的任何變化都會改變$scope.full_name
,因此循環需要再次執行以確認不再有任何變化了。
因為這里只改變了$scope.name
屬性,并沒有改變$scope
對象中的其他任何屬性,所以$digest
循環會退出,然后瀏覽器會重繪DOM以刷新視圖。
當用戶在輸入字段中輸入他們的名字并點擊提交按鈕時,會引發一個略有不同的流程。
ng-click
為DOM元素綁定了瀏覽器原生的click
事件。當這個DOM元素收到點擊事件時,ng-click
指令會調用$scope.$apply()
,同時進入$digest
循環。
$evalAsync 列表
$evalAsync()
方法是一種在當前作用域上調度表達式在未來某個時刻運行的方式。$digest
循環運行的第二個操作是執行$$asyncQueue
??梢允褂?code>$evalAsync()方法訪問這個工作隊列。
$digest
循環期間,貫穿臟值檢查生命周期的每個循環之間的隊列都是空的,這意味著使用$evalAsync
來調用任何函數都會發生兩件事情。
- 函數會在這個方法被調用的某個時刻之后執行。
- 表達式求值之后至少會執行一次
$digest
循環。
$evalAsync()
方法接受一個唯一參數:expression
(字符串/函數)。
這個表達式便是我們想要在當前作用域上執行的東西。如果傳入一個字符串,Angular將會在當前作用域上使用$eval
求值該表達式。
如果傳入的是一個函數,Angular將會使用傳遞給這個函數的scope
對象執行函數求值。
$scope.$evalAsync('attribute',function(scope) {
scope.foo = "Executed"
});
使用$evalAsync
時要注意的一些細節。
- 如果指令直接調用
$evalAsync()
,它會在Angular操作DOM之后、瀏覽器渲染之前運行。 - 如果控制器調用
$evalAsync()
,它也會在Angular操作DOM之后、瀏覽器渲染之前運行(永遠不要使用$evalAsync()
來約定事件的順序)。
無論何時,在Angular中,只要你想要在一個行為的執行上下文外部執行另一個行為,就應該使用$evalAsync()
函數。
你還可以使用它替代setTimeout()
函數,但是它可能在瀏覽器重新渲染視圖之后導致屏幕閃爍。
$apply
$apply()
函數可以從Angular框架的外部讓表達式在Angular上下文內部執行。例如,假設你實現了一個setTimeout()
或者使用第三方庫并且想讓事件運行在Angular上下文內部時,就必須使用$apply()
。
$apply()
函數接受一個可選的參數:expression
(字符串/函數)。
這個表達式可選地接受一個字符串或函數,并且是在當前作用域內執行。
如果傳入一個字符串,$apply()
首先會在這個字符串上調用$eval()
,以強制Angular在局部作用域上下文中使用$eval()
運行字符串表達式。
如果傳入一個函數,這個函數將會在所傳入的函數作用域上執行。
$exceptionHandler
服務會捕獲和處理$eval()
方法拋出的所有異常。最后,$apply()
方法還會直接調用$digest
循環。
// 使用要eval的字符串調用$apply示例
$scope.$apply('message = "Hello World"');
// 使用函數的方式并給函數傳入一個作用域
$scope.apply(function(scope) {
// 然后在函數中使用傳入作用域
scope.message = "Hello World";
});
// 使用函數時忽略作用域
$scope.$apply(function() {
$scope.message = "Hello World";
});
// 或者通過在操作的尾部調用$apply()以強制運行$digest循環
$scope.apply();
簡而言之,使用$scope.$apply()
時可以從外部進入上下文。
如果在事件被觸發時調用$apply()
,就會使用Angular事件循環來運行它。如果沒有調用$apply()
,就不會在事件循環內執行這個函數,而它會運行在Angular上下文外部。
何時使用$apply
通??梢砸蕾囉贏ngular提供的可用于視圖中的任意指令來調用$apply()
。所有ng-[event]
指令(比如ng-click
、ng-keypress
)都會調用$apply()
。
此外還可以依賴于一系列Angular內置的服務來調用$digest()
。比如$http
服務會在XHR請求完成并觸發更新返回值(promise
)之后調用$apply()
。
無論何時我們手動處理事件,使用第三方框架(比如jQuery
),或者調用setTimeout()
,都可以使用$apply()
函數讓Angular返回$digest
循環。
一般不建議在控制器中使用$apply()
,因為這樣會導致難以測試,而且如果不得不在控制器中使用$apply()
或者$digest()
,很可能讓事情變得更加難以理解。
當我們將jQuery和Angular集成在一起時,就需要使用$apply()
,因為Angular不會察覺到執行在Angular上下文外部的事件。例如,在使用jQuery插件時,就需要使用$apply()
將來自jQuery的值傳遞到Angular應用中。
在這里,我們構建了一個簡單的指令,指令中我們在元素上使用了datepicker
這個jQuery插件方法。datepicker
插件暴露了一個onSelect
事件,這個事件會在用戶選擇日期時觸發。為了在Angular應用內部獲取用戶選擇的日期,我們需要在$apply()
函數內運行datepicker
的回調函數。
ele.datepicker()
函數是由jQuery datepicker
插件提供的可用于DOM元素的屬性方法。ctrl.$setViewValue()
函數是在DOM元素上使用ng-model
時提供的指令。
app.directive('myDatepicker',function() {
return function(scope,ele,attrs,ctrl) {
$(function() {
// 在元素上調用datepicker方法
ele.datepicker({
dateFormat: 'mm/dd/yy',
onSelect: function(date) {
scope.$apply(function() {
ctrl.$setViewValue(date);
});
}
});
});
}
});