Angular開發者指南(六)做用域

什麼是做用域?
做用域是引用應用程序模型的對象。 它是表達式的執行上下文。 做用域以層次結構排列,模仿應用程序的DOM結構,它能夠觀察表達式和傳播事件。
做用域的特徵
Scope提供API($watch)來觀察模型改變。
Scope提供API($apply),經過系統將任何模型更改傳播到"AngularJS領域"(控制器,服務,AngularJS事件處理程序)外部的視圖中。
Scope能夠嵌套以限制對應用程序組件的屬性的訪問,同時提供對共享模型屬性的訪問。 嵌套的做用域是「子做用域」或「隔離做用域」。 「子做用域」(原型)從其父做用域繼承屬性。 「隔離做用域」不從父做用域中繼承屬性。
Scope提供對其評估表達式的上下文。 例如{{username}}表達式沒有意義,除非根據定義username屬性的特定做用域進行求值。
做用域做爲數據模型
Scope是應用程序控制器和視圖之間的粘合劑。 在模板連接階段,指令在做用域上設置$watch表達式。 $watch容許通知屬性更改的指令,這容許指令將更新的值呈現給DOM。
控制器和指令都涉及做用域,但不是互相。 這種措施將控制器與指令以及DOM隔離。 這是一個重要的點,由於它使控制器被視爲不存在,這極大地改善了應用程序的測試環節。css

script.jshtml

angular.module('scopeExample', [])
.controller('MyController', ['$scope', function($scope) {
  $scope.username = 'World';
  $scope.sayHello = function() {
    $scope.greeting = 'Hello ' + $scope.username + '!';
  };
}]);

index.htmlexpress

<div ng-controller="MyController" ng-app="scopeExample">
  Your name:
    <input type="text" ng-model="username">
    <button ng-click='sayHello()'>greet</button>
  <hr>
  {{greeting}}
</div>

在上面的示例中,注意MyController將World指定給做用域的username屬性。 做用域而後通知輸入的分配,而後呈現輸入用用戶名預填充。 這演示了控制器如何將數據寫入做用域。
相似地,控制器能夠將行爲分配給做用域,如sayHello方法所示,當用戶單擊"greet"按鈕時調用。 sayHello方法能夠讀取username屬性並建立一個greeting屬性。這代表做用域上的屬性在綁定到HTML的input控件時自動更新。
邏輯上{{greeting}}的渲染包括:數組

  • 檢索與在模板中定義{{greeting}}的DOM節點相關聯的做用域。在這個示例中,這是與傳遞到MyController的做用域相同的做用域。 (稍後咱們將討論做用域層次結構。)瀏覽器

  • 根據上面檢索的做用域計算greeting語表達式,並將結果分配給包含的DOM元素的文本。服務器

能夠將做用域及其屬性視爲用於呈現視圖的數據。做用域是全部視圖相關的單一真實來源。
從可測試性的角度來看,控制器和視圖的分離是可取的,由於它容許咱們測試行爲而不會被渲染細節分散注意力。網絡

protractor.js數據結構

it('should say hello', function() {
  var scopeMock = {};
  var cntl = new MyController(scopeMock);
  // 預測用戶名已預填
  expect(scopeMock.username).toEqual('World');
  // 預測咱們讀新的用戶名和問候
  scopeMock.username = 'angular';
  scopeMock.sayHello();
  expect(scopeMock.greeting).toEqual('Hello angular!');
});

做用域層次
每一個AngularJS應用程序只有一個根做用域,但能夠有任意數量的子做用域。
應用程序能夠有多個做用域,由於指令能夠建立新的子做用域。 建立新做用域時,它們被認爲是添加到父做用域的子做用域。 這建立了一個樹結構,它與它們附加的DOM平行。
當AngularJS計算{{name}}時,它首先查看與name屬性的給定元素相關聯的做用域。 若是沒有找到這樣的屬性,它搜索父做用域,等等,直到達到根做用域。 在JavaScript中,這種行爲被稱爲原型繼承,而子做用域原型繼承自他們的父母。
此示例說明了應用程序中的做用域,以及屬性的原型繼承。示例後面是描述做用域邊界的圖。app

index.html異步

<div class="show-scope-demo" ng-app="scopeExample">
  <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>

script.js

angular.module('scopeExample', [])
.controller('GreetController', ['$scope', '$rootScope', function($scope, $rootScope) {
  $scope.name = 'World';
  $rootScope.department = 'AngularJS';
}])
.controller('ListController', ['$scope', function($scope) {
  $scope.names = ['Igor', 'Misko', 'Vojta'];
}]);

style.css

.show-scope-demo.ng-scope,
.show-scope-demo .ng-scope  {
  border: 1px solid red;
  margin: 3px;
}

圖片描述

請注意,AngularJS自動將ng-scope類放置在附加了做用域的元素上。 此示例中的<style>定義以紅色突出顯示新做用域位置。 子做用域是必需的,由於repeater計算{{name}}表達式,可是根據表達式的做用域來計算,它會產生不一樣的結果。 一樣,{{department}}的計算,它的做用域原型從根做用域繼承,由於它是惟必定義了department屬性的地方。
從DOM中檢索做用域
做用域做爲$scope數據屬性附加到DOM,而且能夠檢索以用於調試目的。 (這不太可能須要在應用程序內以這種方式檢索做用域。)根做用域附加到DOM的位置由ng-app指令的位置定義。 一般,ng-app放置在<html>元素上,但也能夠放置在其餘元素上,例如,只有一部分視圖須要由AngularJS控制。
要檢查調試器的做用域:

  1. 在瀏覽器中右鍵單擊感興趣的元素,而後選擇「檢查元素」。 應該看到瀏覽器調試器與你點擊的元素突出顯示。

  2. 調試器容許以$0變量訪問控制檯中當前選定的元素。

  3. 在控制檯中執行檢索相關聯的做用域:angular.element($0).scope(),
    scope()函數僅在$compileProvider.debugInfoEnabled()爲true(這是默認值)時可用。

做用域事件傳播
做用域能夠以相似的方式將事件傳播到DOM事件。 事件能夠廣播到當前以及子做用域或發射到當前以及父做用域。

angular.module('eventExample', [])
.controller('EventController', ['$scope', function($scope) {
  $scope.count = 0;
  $scope.$on('MyEvent', function() {
    $scope.count++;
  });
}]);

index.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>

做用域生命週期
接收事件的瀏覽器的正常流程是它執行相應的JavaScript回調。一旦回調完成,瀏覽器從新呈現DOM並返回等待更多事件。
當瀏覽器調用JavaScript時,代碼在AngularJS執行上下文以外執行,這意味着AngularJS不知道模型修改。爲了正確處理模型修改,執行必須使用$apply方法輸入AngularJS執行上下文。只有在$apply方法中執行的模型修改纔會被AngularJS適當地考慮。例如,若是指令偵聽DOM事件,例如ng-click,它必須計算$apply方法中的表達式。
在計算表達式以後,$apply方法執行$digest。在$digest階段,做用域檢查全部$watch表達式,並將它們與先前的值進行比較。這種髒檢查是異步完成的。這意味着如$scope.username ="angular"的賦值不會當即致使$watch被通知,而$watch通知被延遲到$digest階段。這種延遲是可取的,由於它將多個模型更新合併成一個$watch通知,以及保證在$watch通知期間沒有其餘$watch正在運行。若是$watch改變模型的值,它將強制額外的$digest週期。

  • 建立
    根做用域在$injector的應用程序引導期間建立。在模板連接期間,一些指令建立新的子做用域。

  • 觀察者註冊
    在模板連接期間,指令在做用域上註冊watches。這些watches將用於將模型值傳播到DOM。

  • 模型改變
    爲了正確觀察改變,你應該使它們只在scope.$apply()。 AngularJS API隱式執行此操做,所以在控制器中執行同步工做或使用$http,$timeout或$interval服務進行異步工做時,不須要額外的$apply調用。

  • 改變觀察
    在$apply結束時,AngularJS對根做用域執行$digest循環,而後在全部子做用域中傳播。在$digest週期中,檢查全部被$watch監控的表達式或函數的模型改變,若是檢測到改變,則調用$watch監聽器。

  • 做用域銷燬
    當再也不須要子做用域時,子做用域建立器負責經過scope.$destroy()API銷燬它們。這將中止$digest調用傳播到子做用域,並容許由子做用域模型使用的內存由垃圾回收器回收。

做用域和指令
在編譯階段,編譯器compiler將指令directives與DOM模板匹配。 指令一般屬於兩種類型之一:

  • 觀察指令,例如雙花括號達式{{expression}},使用$watch()方法註冊監聽器。 這種類型的指令須要在表達式更改時通知,以便它能夠更新視圖。

  • 監聽器指令,例如ng-click,向DOM註冊監聽器。 當DOM偵聽器觸發時,指令執行關聯的表達式並使用$apply()方法更新視圖。

當接收到外部事件(例如用戶操做,定時器或XHR)時,必須經過$apply()方法將關聯的表達式應用於做用域,以便正確更新全部偵聽器。
建立做用域的指令
在大多數狀況下,指令和做用域交互,但不建立做用域的新實例。 然而,一些指令,例如ng-controller和ng-repeat,建立新的子做用域,並將子做用域附加到相應的DOM元素。
一種特殊類型的做用域是隔離做用域,它不會從父做用域繼承原型。 這種類型的做用域對於應該與其父做用域隔離的組件指令很是有用。
還要注意,使用.component()幫助程序建立的組件指令始終建立隔離做用域。
控制器和做用域
做用域和控制器在如下狀況下相互交互:

  • 控制器使用做用域將控制器方法暴露給模板。

  • 控制器定義能夠改變模型(做用域上的屬性)的方法(行爲)。

  • 控制器能夠在模型上註冊watch。 這些監視在控制器行爲執行後當即執行。

做用域$watch性能注意事項
髒檢查更改做用域上的屬性是AngularJS中的常見操做,所以髒檢查函數必須有效。 應該注意髒檢查函數不要作任何DOM訪問,由於DOM訪問比JavaScript對象的屬性訪問慢幾個數量級。
做用域$watch延伸
可使用三種策略進行髒檢查:經過引用,按集合內容和按值。 策略在它們檢測到的變化的種類和它們的性能特徵方面不一樣。

  • 經過引用觀察(scope.$watch(watchExpression, listener)當watch表達式返回的整個值切換到新值時檢測到更改。 若是值是數組或對象,則不會檢測到其中的更改。 這是最有效的策略。

  • 觀察集合內容(scope.$watchCollection(watchExpression, listener))檢測在數組或對象內發生的更改:添加,刪除或從新排序項目時。 檢測很淺 - 它不能到達嵌套集合。 觀察集合內容比經過引用觀察更昂貴,由於集合內容的副本須要維護。 可是,該策略嘗試最小化所需的複製量。

  • 按值觀察(scope.$watch(watchExpression,listener,true))檢測任意嵌套數據結構中的任何變化。 它是最強大的變化檢測策略,但也是最昂貴的。 每一個摘要都須要徹底遍歷嵌套數據結構,而且須要在內存中保存它的完整副本。
    下面是示例的圖例

圖片描述

與瀏覽器事件循環集成
下面的圖和下面的例子描述了AngularJS如何與瀏覽器的事件循環交互。
圖片描述

  1. 瀏覽器的事件循環等待事件到達。事件是用戶交互,定時器事件或網絡事件(來自服務器的響應)。

  2. 事件的回調被執行。這將進入JavaScript上下文。回調能夠修改DOM結構。

  3. 一旦回調執行,瀏覽器將保留JavaScript上下文並根據DOM更改從新呈現視圖。

AngularJS經過提供本身的事件處理循環來修改正常的JavaScript流程。這將JavaScript分爲經典和AngularJS執行上下文。只有在AngularJS執行上下文中應用的操做才能受益於AngularJS數據綁定,異常處理,屬性監視等等。還可使用$apply()從JavaScript中輸入AngularJS執行上下文。請記住,在大多數地方(控制器,服務)$apply已經由處理事件的指令調用。只有在實現自定義事件回調或使用第三方庫回調時,才須要顯式調用 $apply。

  1. 經過調用scope.$apply(stimulusFn)進入AngularJS執行上下文,其中stimulusFn是但願在AngularJS執行上下文中執行的工做。

  2. AngularJS執行stimulusFn(),它一般修改應用程序狀態。

  3. AngularJS進入$digest循環。循環由兩個較小的循環組成,它們處理$evalAsync隊列和$watch列表。 $digest循環繼續迭代,直到模型穩定,這意味着$evalAsync隊列爲空,而且$watch列表未檢測到任何更改。

  4. $evalAsync隊列用於調度須要在當前堆棧幀以外發生的工做,但在瀏覽器的視圖呈現以前。這一般是經過setTimeout(0)完成的,可是setTimeout(0)方法會受到緩慢的影響,而且可能會致使視圖閃爍,由於瀏覽器在每一個事件後呈現視圖。

  5. $watch列表是一組自上次迭代以來可能已更改的表達式。若是檢測到更改,則調用$watch函數,一般使用新值更新DOM。

  6. 一旦AngularJS的$digest循環完成,執行離開AngularJS和JavaScript上下文。隨後瀏覽器從新呈現DOM以反映任何更改。

如下是當用戶在文本字段中輸入文本時,Hello world示例如何實現數據綁定效果的說明。

  1. 在編譯階段:
    1.ng-model和input指令在<input>控制上設置一個keydown監聽器。
    2.插值設置一個$watch以通知名稱更改。

  2. 在運行時階段: 1.按"X"鍵使瀏覽器在輸入控件上發出按鍵事件。 2.輸入指令捕獲對輸入值的更改,並調用$apply("name ='X';")來更新AngularJS執行上下文中的應用程序模型。 3.AngularJS應用"name ='X';到模型。 4.$digest循環開始 5.$watch列表檢測name屬性的更改,並通知插值,這反過來更新DOM。 6.AngularJS退出執行上下文,這反過來退出keydown事件和它的JavaScript執行上下文。 7.瀏覽器使用更新的文本從新呈現視圖。

相關文章
相關標籤/搜索