AngularJS講義 - 做用域

什麼是做用域?html

  Angular中做用域(scope)是模板以及工做的上下文環境,做用域中存放了應用模型和視圖相關的回調行爲。做用域是層次化結構的與相關聯的DOM結構相對應。做用域能夠觀察表達式以及傳播事件。前端

  原文: scope is an object that refers to the application model. It is an execution context for expressions. Scopes are arranged in hierarchical structure which mimic the DOM structure of the application. Scopes can watch expressions and propagate events.angularjs

做用域的特性ajax

  做用域提供了相關的APIs($watch)來監控模型的狀態而且將Angular系統(視圖、服務、事件處理器)內部的模型的變化同步到視圖。express

  做用域能夠嵌套來控制應用組件對模型屬性的訪問。嵌套的做用域能夠是「父子」關係或者"同級"關係。子做用域能夠繼承父做用域的屬性,相鄰做用域是互補可見的。api

  做用域提供了表達式的上下文環境。例如表達式{{username}}只有在定義了username屬性的做用域中才有意義。數組

做用域做爲數據模型瀏覽器

  做用域是鏈接Angular控制器和視圖的中間地帶。指令會在模板連接階段(linking)在做用域中創建對錶達式的監控($watch)服務。這樣$watch就能夠將模型屬性的變化狀況及時通知給指令從而更新視圖。網絡

  控制器和指令只能經過做用域鏈接,不能夠直接關聯。這樣就實現了控制器和視圖的解耦。這樣就能夠實現一套模型綁定多個視圖,也提升了前端代碼的可測性。數據結構

   index.html

Html代碼   收藏代碼
 1 <div ng-controller="MyController">  
 3   Your name:  
 5 <input type="text" ng-model="username">  
 7 <button ng-click='sayHello()'>greet</button>  
 9 <hr>  
11   {{greeting}}  
13 </div>  

 

   script.js

Js代碼   收藏代碼
1 angular.module('scopeExample', [])  
2 .controller('MyController', ['$scope', function($scope) {  
3   $scope.username = 'World';  
5   $scope.sayHello = function() {  
6     $scope.greeting = 'Hello ' + $scope.username + '!';  
7   };  
8 }]);  

    上述例子說明了做用域的工做原理:

      1. 在MyController控制器中定義了username屬性, 並在輸入文本控件中綁定了該屬性。username被初始化爲'World',這樣做用域會通知文本框中並在文本框預填入初始值。

      2. 一樣控制器在做用域中定義了sayHello行爲,並經過ng-click註冊到按鈕的點擊事件。當用戶在input中輸入其餘值時,會經過做用域更新username屬性,從而改變sayHello的結果。

   運行結果:

    {{greeting}}表達式的工做原理以下:

    1. 先找到{{greeting}}表達式所在DOM相關的做用域。在此例中爲MyController中的$scope.

    2. 在做用域中找到greeting屬性並替換{{greeting}}, 既而更新了視圖。

    scope及其屬性提供了用來展示視圖的數據。(原文: The scope is the single source-of-truth for all things view related.)

   從測試的角度考慮, 視圖與控制器分離是必要的, 這樣咱們就能夠單獨測試視圖後面的行爲而不用考慮視圖的細節。

Js代碼   收藏代碼
 1 it('should say hello', function() {  
 2   var scopeMock = {};  
 3   var cntl = new MyController(scopeMock); 
 5   // Assert that username is pre-filled  
 6   expect(scopeMock.username).toEqual('World');  
 8   // Assert that we read new username and greet  
 9   scopeMock.username = 'angular';  
10   scopeMock.sayHello();  
11   expect(scopeMock.greeting).toEqual('Hello angular!');  
12 });  

做用域的層次化結構

   每一個Angular應用都有一個根做用域(root scope), 在根做用域下能夠有多個子做用域。一些指令(directives)也會建立子做用域。 新的做用域會被添加到相應的父做用域上,這樣就造成了與DOM視圖相平行的樹形結構。

  讓咱們經過一個具體的例子來理解:

Html代碼   收藏代碼
 1 <div class="show-scope-demo">  
 2   <div ng-controller="GreetController">  
 3     Hello {{name}}!  
 4   </div>  
 5   <div ng-controller="ListController">  
 6     <ol>  
 7       <li ng-repeat="name in names">{{name}} from {{department}}</li>  
 8     </ol>  
 9   </div>  
10 </div>  
Js代碼   收藏代碼
1 angular.module('scopeExample', [])  
2 .controller('GreetController', ['$scope', '$rootScope', function($scope, $rootScope) {  
3   $scope.name = 'World';  
4   $rootScope.department = 'Angular';  
5 }])  
6 .controller('ListController', ['$scope', function($scope) {  
7   $scope.names = ['Igor', 'Misko', 'Vojta'];  
8 }]);  
Css代碼   收藏代碼
1 .show-scope-demo.ng-scope,  
2 .show-scope-demo .ng-scope  {  
3   border: 1px solid red;  
4   margin: 3px;  
5 }  
6  

  輸出:

   對應的DOM結構:

  

咱們能夠注意到Angular會自動給綁定做用域的DOM元素加上"ng-close"類, CSS文件中給ng-scope類的元素加了高亮顯示。爲<li>建立子做用域是必要的,由於每一個<li>都會有{{name}}表達式指向本身的name屬性。{{department}}中department則繼承根做用域$rootScope.department屬性。

獲取DOM的做用域

      做用域是與視圖相關聯的,咱們能夠在debug的時候經過api獲取綁定到視圖的做用域。根做用域(root scope)定義在含有ng-app指令的DOM元素上。一般ng-app放在<html>元素中, 固然ng-app也能夠放在任何DOM元素上,例如咱們只想局部視圖被angular控制。

     在Chrome中咱們只需右擊而後選擇「審查元素」選項進入調試界面。

     經過$0即可得到當前選中的DOM元素。

     經過angular.element($0).scope()或者$scope能夠得到當前元素對應的做用域。

做用域事件傳播

    相似DOM事件,咱們能夠在做用域間傳播事件。事件能夠被廣播($broadcast)到子做用域或者向上傳播到父做用($emit)域中。

    讓咱們看具體的例子:

Html代碼   收藏代碼
 1 <div ng-controller="EventController">  
 2   Root scope <tt>MyEvent</tt> count: {{count}}  
 3   <ul>  
 4     <li ng-repeat="i in [1]" ng-controller="EventController">  
 5       <button ng-click="$emit('MyEvent')">$emit('MyEvent')</button>  
 6       <button ng-click="$broadcast('MyEvent')">$broadcast('MyEvent')</button>  
 7       <br>  
 8       Middle scope <tt>MyEvent</tt> count: {{count}}  
 9       <ul>  
10         <li ng-repeat="item in [1, 2]" ng-controller="EventController">  
11           Leaf scope <tt>MyEvent</tt> count: {{count}}  
12         </li>  
13       </ul>  
14     </li>  
15   </ul>  
16 </div>  
Js代碼   收藏代碼
1 angular.module('eventExample', [])  
2 .controller('EventController', ['$scope', function($scope) {  
3   $scope.count = 0;  
4   $scope.$on('MyEvent', function() {  
5     $scope.count++;  
6   });  
7 }]);  
8  

   在HTML模板中,咱們經過ng-click註冊了點擊事件監聽,分別來向子做用域廣播和向上傳播MyEvent時間,在控制器咱們經過$scope.$on('MyEvent')監聽事件。

   運行結果以下:

 做用域的生命週期

 一般瀏覽器接收一個事件時會執行一段Javascript回調執行相關的處理,等回調執行完會自動更新DOM繼續等待處理新事件。當瀏覽器調用的js代碼不在angular執行上下文中,angular將不會注意模型的更改。咱們能夠經過$apply方法,讓模型的更改在angular的上下文中進行。也就是說只有在$apply過程當中的模型更改纔會被angular甄別。例如,某個指令監聽DOM事件(好比ng-click),必須在$apply方法中執行angular表達式。

 在表達式執行完成後,$apply會執行$digest。在$digest過程當中,做用域會檢查全部$watch的表達式,比較表達式的當前狀態和上一次狀態。這種髒數據(dirty data)的檢查是異步的。也就是說給模型屬性賦予新的值時,$watch不會當即被通知,通知$watch發生在$digest階段。這個短暫的延遲是有緣由的,angular會批量通知$watch模型的狀態狀況,而且保證同時只有一個$watch在進行。若是$watch改變了模型的狀態,會再強行觸發一次$digest過程:

   1. 建立 - 更做用域會在應用啓動時經過注入器建立並注入。在模板鏈接階段,一些指令會建立本身的做用域。

  2.  註冊觀察者 - 在模板鏈接階段,將會註冊做用域的監聽器。這也監聽器被用來識別模型狀態改變並更新視圖。

  3.  模型狀態改變 - 更新模型狀態必須發生在scope.$apply方法中才會被觀察到。Angular框架封裝了$apply過程,無需咱們操心。

  4.  觀察模型狀態 - 在$apply結束階段,angular會從根做用域執行$digest過程並擴散到子做用域。在這個過程當中被觀察的表達式或方法會檢查模型狀態是否變動及執行更新。

  5.  銷燬做用域

       當再也不須要子做用域時,經過scope.$destroy()銷燬做用域,回收資源。

做用域和指令

  在編譯(compiling)階段,angular編譯器會將DOM模板和指令匹配綁定。指令通常可分爲如下兩類:

   1. 觀察指令(Observing directives), 例如表達式, 經過$watch方法註冊監聽。這類指令在表達式變化時會被通知從而更新視圖。

   2. 監聽器指令(Listener directives), 例如ng-click會在DOM元素上註冊監聽事件。DOM事件會觸發指令執行相關的表達式或者經過$apply更新視圖。

  當接收到外部事件時(用戶動做,計時器或者ajax相關的事件),相關做用域的表達式必須經過$apply方法執行,確保全部監聽器狀態被正確的更新。

哪些指令會建立做用域?

  在大多數狀況下,指令不會自行建立本身的做用域。但一些指令,例如ng-controller, ng-repeat等會建立子做用域和DOM綁定。咱們能夠經過angular.element(aDomElement).scope() 獲取和DOM元素相關聯的做用域。

控制器和做用域

  控制器和做用域能夠經過如下方式交互:

   1. 控制器經過做用域暴露給模板相關的行爲。

   2. 控制器定義能夠操做模型的方法。

   3. 控制器能夠經過$watch註冊監聽模型的狀態。這些watch會當即在控制器方法執行後被觸發執行。

  angular會自動檢查做用域內模型狀態的變動,這些檢查並不觸及DOM操做,而只是檢查做用域的屬性。

  出於對性能的考慮,對不一樣數據類型(引用、集合、 值)的檢查會有不一樣的策略(參考圖中的決策樹):


     檢查引用 - 當表達式返回一個新的對象或者數組時,scope.$watch(watchExpression, listener)不會再具體對象裏面的具體內容,而是比較引用是否指向新的內存地址。

     檢查集合的內容 - 當對集合類型的數據增長、刪除元素或者排序時,$scope.$watchCollection(watchExpression, listener)會檢查集合中的元素。對集合中內容的檢查是Shallow的, 即不會檢查嵌套在集合中的集合或者對象。這種策略試圖減小對內存資源的消耗。

     檢查值 - 根據模型的數據結構,scope.$watch (watchExpression, listener, true)深度遍歷數據結構中的每一個域的值,這種策略是最強大的但須要至關大的資源開銷。 

與瀏覽器事件的交互

   咱們將結合下圖分析angular與瀏覽器事件交互的工做原理:

   1. 瀏覽器的事件環路會一直等待事件發生,這裏的事件包括用戶的交互,計時器,網絡事件等。

   2. 事件會觸發監聽器在js上下文環境中調用相關回調方法更新DOM結構。

   3. 回調結束後,瀏覽器會脫離js上下文環境,基於DOM的變更從新渲染視圖。

  angular在以上的Javascript流程中加入了本身的事件處理機制,把js分紅了傳統的js上下文和angular執行的上下文。angular中的操做會得益於angular的數據綁定機制、異常處理和屬性監聽等框架特性。咱們也能夠經過$apply進入angular的執行上下文環境。

   在大多數狀況下(控制器、服務),angular會自動給咱們調用$apply處理事件。除非是實現自定義的事件及回調或者是與第三方庫結合使用時纔會顯示調用$apply進入angular上下文環境。$apply工做步驟以下:

  1. 經過調用scope.$apply(stimulusFn)進入angular上下文環境,stimulusFn爲但願在angular上下文中執行的代碼。

  2. 一般stimulusFn用來改變應用的狀態。

  3. angular進入$digest環路,$digest環路包括兩個子循環分別處理$evalAsync隊列和$watch列表。$digest會持續迭代直到$evalAsync隊列清空而且$watch列表中沒有任何狀態更新。

  4. $evalAsync隊列被用來調度圖中右半部分渲染DOM以前的子任務。

  5. $watch列表中包含了上一次迭代後變化的表達式。當相關的模型變化是,$watch會更新表達式的值並更新視圖。

  6. 當$digest循環終止即離開Angular和Javascript的上下文環境,瀏覽器會隨之更新視圖。

  基於上述原理咱們詳解一下"Hello World"例子中的數據綁定是怎麼實現的:

  1. 在angular編譯階段:

         - ng-model和input指令在<input>控件中創建了"keydown"事件的監聽器

         - angular經過(interpolation)創建$watch對name屬性的跟蹤

  2. 在運行階段:

          - 鍵入’x‘ 即觸發keydown事件, input指令會捕獲輸入的變化調用$apply("name = 'x'"),更新模型數據。

          - 更新完成後進入$digest循環,$watch列表檢測到name屬性發生了變化並通知interpolation,更新DOM視圖。

          - angular退出運行上下文,從而退出了keydown事件和與之相關的js上下文環境。

          - 瀏覽器檢測到DOM變化從新展示視圖。

相關文章
相關標籤/搜索