筆記類文章
angularJS Scope綜述
于2017年1月14日 翻譯自angularjs 官網(wǎng)開發(fā)者指南
每一個應用都僅有一個根域,其他所有域都是它的子孫,scopes將model和view分離開來,并通過一種機制監(jiān)控model變化,它也提供事件的發(fā)散/廣播并且提供設施。
一個根域($rootScope
),通過$injector
加載根域的名字,可以被重新獲取。子域(childScopes
)通過$new()
方法來創(chuàng)建。(大部分的域scope
是在HTML模版編譯完成時自動創(chuàng)建的)
什么是Scope
scope
是一個應用模型對象,它是一個表達式的執(zhí)行上下文。它被用于那些模仿DOM結構的分層結構的應用里。它可以監(jiān)控表達式和傳播事件。
Scope特性
-
Scopes
提供($watch
)來觀察model
的變化 -
Scopes
提供($apply
)來溝通視圖(view
)到angular領域外的系統(tǒng),以便傳播任何model
的變化,(controllers, services, angular event handlers) - 可以通過嵌套scope來限制其對應用程序組件屬性的使用權。嵌套域是
child scopes
或isolate scopes
(獨立子域,directive創(chuàng)建的scope)這種子域。child scope
會從父域繼承屬性,isolate scope
則不會 - Scopes會針對被求值的表達式提供上下文,eg:
{{username}}
表達式是無意義的,除非在一個特定的scope中定義了username
數(shù)據(jù)模型的scope
-
scope
是應用在控制器和視圖之間的粘合劑。在模版的linking
階段,directives
指令在scope上建立$watch
來監(jiān)控屬性的變化(如果變化,通知directive),并允許指令為DOM重新渲染更新后的數(shù)據(jù)。 - 所有的控制器和指令都與scope有關,而不是彼此有關,這種布局將控制器與指令很好的分離開來,就像控制器與DOM一樣。讓控制器變的不可知是很重要的,這大大改善了應用調試時的情況。
- 在邏輯上,渲染dom中如{{greeting}}過程是:
- 遍歷scope關聯(lián)的模版中被定義{{greeting}}的DOM節(jié)點。
- 依據(jù)正在遍歷的scope重新計算表達式,然后重新設置結果。
- 可以把scope和其上屬性想象為用于渲染視圖的data。scope僅僅是所有與視圖有關的實物的實際來源(source-of-truth)。
- 在一個視圖的可測試點(testability point of view?可能是angular的測試模塊的東西ngMock),分離視圖和控制器是不可能的。因為它允許我們檢測行為而不用分心于渲染細節(jié)。
scope 層級
- 每個angular程序都只有一個
root scope
,但有很多的孩子域。 - 應用可以有多個域,因為有一些指令會創(chuàng)建新的子域,新的子域在創(chuàng)建完成后,被當作父域的孩子插入到父域中。這種方式,將在與Dom互相依賴的地方,創(chuàng)建了一顆與dom平行的樹狀結構。
- 來看一個例子:
//html
<div class="show-scope-demo">
<div ng-controller="GreetController">
Hello {{name}}!
</div>
<div ng-controller="ListController">
<ol>
<li ng-repeat="name in names">{{name}} from {{department}}</li>
</ol>
</div>
</div>
//javascript
angular.module('scopeExample', [])
.controller('GreetController', ['$scope', '$rootScope', function($scope, $rootScope) {
$scope.name = 'World';
$rootScope.department = 'Angular';
}])
.controller('ListController', ['$scope', function($scope) {
$scope.names = ['Igor', 'Misko', 'Vojta'];
}]);
域的劃分情況:
- 應當注意:angular自動添加
ng-scope
類名到那些被附加了scope的元素上。 - 子域是必要的,因為重復的對如
{{name}}
這樣的表達式求值,這時,根據(jù)表達式求值時的子域不同,得到不同的結果。類似的,對于{{department}}
的求值,他繼承自根域。
從DOM retrieving(重新檢索?) scopes
- scope以
$scope
這樣的data屬性附加到DOM上,處于檢錯的目的,他們是可以被檢索的。(這不是說,將必須在程序中以這種方式重新檢索scope) -
ng-app
指令定義了root scope將被附加到DOM的哪個位置。對于將ng-app
放在<html>
標簽上,如果一個頁面只有一部分需要被angular控制,那么放在其他的位置會更好。 - 在debugger中檢查scope
- Right click on the element of interest in your browser and select 'inspect element'. You should see the browser debugger with the element you clicked on highlighted
- debugger允許在控制臺中以
$0
變量來使用當前選擇的元素 - 通過
angular.element($0).scope()
來在控制臺重新檢索元素關聯(lián)的scope,或者輸入$scope
也可以。
域事件的傳播
- 在同樣的
fashion
中,scope可以向dom事件
傳播事件,事件可以broadcasted(廣播)給孩子域或emit(發(fā)散)給父域。 - 使用
$emit('事件名')
來發(fā)散給父域,$broadcast('事件名')
廣播給子域。 - 在接受事件的域使用
$on('對應的事件名')
來接受(注意,兩邊的事件名必須一致) - eg:
//html
<div ng-controller="EventController">
Root scope <tt>MyEvent</tt> count: {{count}}
<ul>
<li ng-repeat="i in [1]" ng-controller="EventController">
<button ng-click="$emit('MyEvent')">$emit('MyEvent')</button>
<button ng-click="$broadcast('MyEvent')">$broadcast('MyEvent')</button>
<br>
Middle scope <tt>MyEvent</tt> count: {{count}}
<ul>
<li ng-repeat="item in [1, 2]" ng-controller="EventController">
Leaf scope <tt>MyEvent</tt> count: {{count}}
</li>
</ul>
</li>
</ul>
</div>
angular.module('eventExample', [])
.controller('EventController', ['$scope', function($scope) {
$scope.count = 0;
$scope.$on('MyEvent', function() {
$scope.count++;
});
}]);
scope生命周期
- 瀏覽器普通流在一個事件執(zhí)行相應js回調函數(shù)時才接受它。一旦回調函數(shù)執(zhí)行完畢,瀏覽器會重新渲染dom,并返回到等待事件的狀態(tài)。
- 當瀏覽器調用angular執(zhí)行上下文之外的js代碼時,這意味著angular不會意識到
model
的修改。為了正確的處理model
的改變,需要使用$apply
方法,將執(zhí)行過程加入到angular的執(zhí)行上下文。只有在$spply
中的model處理,才將被angular正確的解釋。比如,一個指令監(jiān)聽dom事件,他必須在$apply
方法中對表達式求值。 - 在對表達式求值后,
$apply
方法將執(zhí)行$digest
.在$digest
階段,scope檢查()所有的$watch
表達式,并且將他們與以前的值進行比較。這個就是臟檢查,它是異步的。這意味著,賦值將(eg:$scope.username = "angular"
)不會立刻通知$watch
,而是延遲到$digest
階段。這種延遲是必要的,因為,它會在一個$watch
中合并大量的model
更新,同時,也保證了,在這個$watch
執(zhí)行的過程中,沒有別的$watch
正在運行。如果在某個$watch
中又改變了model
的值,他會強制觸發(fā)額外的$digest
循環(huán)- Creation
root scope在應用$injector
的引導程序(bootstrap)的過程中被創(chuàng)建,在連接模版(linking)時,一些指令會創(chuàng)建子域。 - Watcher registration (注冊觀察者)
在模版連接(linking)時,指令(directive)將在域(scope)上注冊觀察者(watches),這些觀察者將被用于傳播model
的值到DOM - model mutation(變化)
因為變化需要被正確的觀察到,應確保他們僅僅在scope.$apply()
內部。angular APIs,隱含的做了這樣的處理,所以,在controllers
中進行同步的任務時;或者使用$http,$timeout,$interval
等服務進行異步任務時,不需要額外的調用$apply
. - mutation observation(變化觀察)
所有的$apply
的末尾,angular會在root scope上執(zhí)行一個$digest
循環(huán),這個循環(huán)會波及到所有的孩子域。在$digest
期間,所有被$watch
的表達式或者函數(shù),會被檢查model
是否變化,一旦發(fā)現(xiàn)變化,$watch
的監(jiān)聽者就被調用。 - scope destruction(域的消亡)
當某個子域(child scope)不再不需要了,那么這個子域的創(chuàng)造者有義務通過scope.$destroy()
api來銷毀這個子域。這將停止再向這個子域傳播$digest
調用,并且允許,被這個子域model
占用的內存被garbage collector
回收。
- Creation
scopes 和 directives(域和指令)
在編譯階段(comlilation),compiler
編譯器會針對DOM模版來匹配directives指令,指令通常分為一或兩種類別:
- 觀察者型指令(observing directives),比如兩個花括號的表達式
{{expression}}
,它會通過$watch()
方法注冊監(jiān)聽者。這種類型的指令,只要表達式變化(值的變化)就會被通知,所以它可以更新視圖數(shù)據(jù)。 - 監(jiān)聽者型指令(listener directives),比如
ng-click
,在DOM上注冊一個監(jiān)聽者,當這個dom監(jiān)聽者激發(fā)時,這種指令會執(zhí)行與其相關聯(lián)的表達式并且通過$apply()
方法來更新視圖(view)
當接收一個外部的事件時,它關聯(lián)的表達式必須通過$apply()
方法應用到scope,以保證所有的監(jiān)聽者正確的更新。
創(chuàng)造域的指令
在大多數(shù)情況下,指令和域將繼承而不是創(chuàng)造,一個新的scope實例。然而,有的指令,比如ng-controller
,ng-repeat
會創(chuàng)造一個新的子域并且將這個子域關聯(lián)到相應的DOM元素上,可以在任何DOM元素上,通過angular.element(aDomElement).scope()
方法來檢索scope。
控制器和域
域和控制器在以下情況將會互相影響
- 控制器通過scope暴露控制器的方法
- 控制器定義可以影響
model
(scope上的屬性)的方法時。 - 控制器可能會在
model
上注冊watches
,這些watches
會在控制器行為執(zhí)行后,立刻執(zhí)行。
scope watch
性能注意事項
臟檢查scope屬性的變化,在angularz中是一個公共的操作,所以臟檢查函數(shù)需要很高效。需要注意,在臟檢查函數(shù)中,避免過多的DOM操作,因為,DOM操作比在js對象上操作屬性慢的多的。
Scope$watch
depths 深度域$watch
如圖:
完成臟檢查有三種策略,引用(by reference),數(shù)據(jù)集合(by collection items),值(by value)。這些策略的不通點在于他們察覺的變化的種類,和他們各自的工作特性(運行方式)。
- 通過引用監(jiān)聽(
scope.$watch(watchExpression, listener)
),當監(jiān)聽表達式轉變?yōu)橐粋€新的值并返回完整的值時,可以察覺到變化。如果返回值是一個array或者object,那么他們內部的變化將不會被察覺到,這是最有效率的模式 - 觀察數(shù)據(jù)集合 (
scope.$watchCollection(watchExpression, listener)
),會察覺到在數(shù)組或對象內部的變化:當item被添加,移除或者重新排序。它是一個淺觀測——即,不會觀測到嵌套結構下的數(shù)據(jù)集合(就是對象內嵌對象查不到)。這種策略比上一種代價高,因為需要維持一個數(shù)據(jù)集合的副本。但這個策略也會企圖使復制
請求的總數(shù)盡量的少。 - value監(jiān)聽(
$scope.$watch(watchExpression, listener, true)
),察覺到所有的變化,(各種嵌套數(shù)據(jù)結構之類的,都會檢查),這是最全面的檢查變化策略,但是也是代價最大的,在每一個digest
都必須完全遍歷嵌套的數(shù)據(jù)結構,并且在內存中維持一個完全拷貝的的副本
集成瀏覽器事件循環(huán)
上圖和接下來的例子將會描述,angular是如何集成瀏覽器的事件循環(huán)
- 瀏覽器的事件循環(huán)等待一個事件的到達。(用戶交互事件,timer事件,網(wǎng)絡任務事件)
- 某事件的回調函數(shù)在js上下文中得到執(zhí)行,這個回調函數(shù)可以修改dom結構。
- 一旦回調函數(shù)得到執(zhí)行,瀏覽器就離開javascript上下文,并且根據(jù)dom的變化重繪視圖(view)。
angular通過提供它自己的事件執(zhí)行循環(huán),修改了普通javascript流,它將javascript分成了普通上下文和angular執(zhí)行上下文,只有應用在angular執(zhí)行上下文才能具有angular的數(shù)據(jù)綁定,異常處理,屬性監(jiān)控,等等。當然,也可以使用$apply()
方法從javascript進入angular執(zhí)行環(huán)境。要記住,在很多地方(controllers,services)$apply
已經(jīng)被那些正在處理事件的指令調用過了。僅僅在執(zhí)行自定義事件回調函數(shù)或者需要運行第三方庫的回調函數(shù)的時候使用$apply()
。
- 通過調用
scope.$apply(stimulusfn)
來進入angular執(zhí)行上下文,stimulusfn是你希望在angular執(zhí)行上下文中運行的任務 - angular運行
stimulusFn()
來修改應用的狀態(tài)。 - angular進入
$digest
循環(huán),這個循環(huán)是建立在兩個更小的循環(huán)之上的:分別用于處理$evalAsync
隊列和$watch
列表。$digest
將會持續(xù)迭代,直到model
穩(wěn)定——即$evalAsync
隊列為空且$watch
列表不檢查任何變化。 -
$evalAsync
隊列常用于安排那些需要在當前堆棧外,但在瀏覽器更新視圖前發(fā)生的任務,通常使用setTimeout(0)
實現(xiàn),但是,setTimeout(0)
方法很遲鈍并且可能導致視圖閃爍,因為瀏覽器會在任何一個事件后渲染視圖。 -
$watch
是一個列表,保存著由于最近一次迭代而發(fā)生改變的表達式們。如果一個改變被檢查到,那么就調用那些以新值更新DOM的$watch
函數(shù)。 -
一旦
angular的$digest
循環(huán)完成,執(zhí)行上下文將離開angular和javascript。接下來瀏覽器重構DOM來反映出所有的變化。
下面解釋一下hello word
例子中,當用戶輸入文本時是如何實現(xiàn)數(shù)據(jù)綁定的效果的。
- 在編譯階段
-
ng-model
和input directive
在<input>
建立一個keydown
監(jiān)聽者 -
interpolation
(插入者?處理器?)建立一個$watch
以便name變化時通知它。
-
- 在運行階段
- 按下某個鍵盤鍵
x
,會導致瀏覽器在<input>
控制器上發(fā)散一個keydown事件 -
input
指令捕獲到輸入值的變化,并調用$apply("name = 'x';")
方法來在angular執(zhí)行上下文中更新應用model
- angular將
name = "x"
應用到model
上 -
$digest
循環(huán)開始 -
$watch
列表檢查到在name
屬性上有一個變化,并且通知interpolation
,interpolation
輪流更新DOM - angular退出那些,連帶javascript執(zhí)行環(huán)境一起輪流退出
keydown
事件的,執(zhí)行上下文。 - 瀏覽器以新值重新渲染view
- 按下某個鍵盤鍵
繼承性
一個域可以從父域繼承
在測試
scope
的相互作用時,為scope
的實例添加一些額外的幫助函數(shù)是很有效的,這些函數(shù)在ngMock
中有介紹
expect(exp).toEqual(value)
計算exp并與后邊的value做比較