AngularJS開發指南9:AngularJS做用域的詳解

AngularJS做用域是一個指向應用模型的對象。它是表達式的執行環境。做用域有層次結構,這個層次和相應的DOM幾乎是同樣的。做用域能監控表達式和傳遞事件。html

做用域的特色

  • 做用域提供APIs($watch)來觀察模型的變化。
  • 做用域提供APIs($apply)將任何模型的改變,反映到視圖上。
  • 做用域能經過共享模型成員的方式嵌套到應用組件上。一個做用域從父做用域繼承屬性。
  • 做用域提供表達式執行的上下文。好比說表達式{{username}}自己是無心義的,除非把它放到指定username屬性的做用域中。

做爲數據模型的做用域

做用域是控制器和視圖之間的「膠水」。在模板連接階段,指令設置好做用域的$watch表達式。$watch使得指令能知曉屬性的改變,這使得指令能從新渲染和更新DOM中的值。angularjs

控制器和指令都持有做用域的引用,可是不持有對方的引用。這使得控制器能從指令和DOM中脫離出來。這很重要,由於這使得控制器徹底不須要知道view的存在,這大大改善了應用的測試。express

舉個例子:瀏覽器

<!doctype html>
<html ng-app>
  <head>
    <script src="http://code.angularjs.org/angular-1.0.2.min.js"></script>
    <script src="script.js"></script>
  </head>
  <body>
    <div ng-controller="MyController">
      Your name:
        <input type="text" ng-model="username">
        <button ng-click='sayHello()'>greet</button>
      <hr>
      {{greeting}}
    </div>
  </body>
</html>

script.js:app

function MyController($scope) {
  $scope.username = 'World';

  $scope.sayHello = function() {
    $scope.greeting = 'Hello ' + $scope.username + '!';
  };
}

上例中MyController將值World賦給了做用域中的username。而後做用域將這個賦值的操做通知給input,而後input就會被渲染成預填充了值的樣子。這展現控制器如何將數據寫入到做用域。函數

一樣的,控制器能將行爲添加到做用域,正如你看到的sayHello方法,這個方法是在用戶點擊'greet'按鈕時被調用的。測試

邏輯上來講,表達式{{greeting}}的渲染須要:spa

  • 獲取模板中定義了{{greeting}}DOM節點相關的做用域。在這個例子裏,就是傳入到MyController的做用域。
  • 結合上一步獲取到的做用域來計算表達式的值,將該值在DOM中替換掉表達式。

你能夠把做用域和它的屬性當作是用來渲染視圖的數據。做用域是視圖惟一相關聯的變化來源。調試

做用域層級

每個AngularJS應用都有一個絕對的根做用域。可是可能有多個子做用域。code

一個應用能夠有多個做用域,由於有一些指令會生成新的子做用域(參考指令的文檔看看哪些指令會建立新做用域)。當新做用域被建立的時候,他們會被當成子做用域添加到父做用域下,這使得做用域會變成一個和相應DOM結構一個的樹狀結構。

當AngularJS執行表達式{{username}},它會首先查找和當前節點相關的做用域中叫作username的屬性。若是沒找到,那就會繼續向上層做用域搜索,直到根做用域。在Javascript中,這被稱爲原型類型的繼承,子做用域以原型的形式繼承自父做用域。

下面這個例子展現了應用中的做用域,它們的繼承關係。

<!doctype html>
<html ng-app>
  <head>
    <script src="http://code.angularjs.org/angular-1.0.2.min.js"></script>
    <script src="script.js"></script>
  <style>
    .doc-example-live .ng-scope {
       border: 1px dashed red;     }
  </style>
  </head>
  <body>
    <div ng-controller="EmployeeController">
      Manager: {{employee.name}} [ {{department}} ]<br>
      Reports:
        <ul>
          <li ng-repeat="employee in employee.reports">
            {{employee.name}} [ {{department}} ]
          </li>
        </ul>
      <hr>
      {{greeting}}
    </div>
  </body>
</html>

script.js:

function EmployeeController($scope) {
  $scope.department = 'Engineering';
  $scope.employee = {
    name: 'Joe the Manager',
    reports: [
      {name: 'John Smith'},
      {name: 'Mary Run'}
    ]
  };
}

注意看成用域和元素相關聯的時候,AngularJS會自動給相應元素添加ng-scope類名。這個例子中的做用域範圍突出顯示了。子做用域的存在是頗有必要的,由於迭代器要執行{{employee.name}}表達式,它會根據不一樣的做用域生成不一樣的值。一樣的,{{department}}的執行是繼承自根做用域的,由於只有根做用域中定義了它。

從DOM中獲取做用域

做用域是做爲$scope的數據屬性關聯到DOM上的,而且能在須要調試的時候被獲取到。根做用關聯的DOM就是ng-app指令定義的地方。通常來講ng-app都是放在<html>元素中的,可是也能放在其餘元素中。

在控制檯中獲取關聯的做用域:angular.element($0).scope()

做用域事件的傳遞

做用域中的事件傳遞是和DOM事件傳遞相似的。事件能夠廣播給子做用域或者傳遞給父做用域。舉個例子:

<!doctype html>
<html ng-app>
  <head>
    <script src="http://code.angularjs.org/angular-1.0.2.min.js"></script>
    <script src="script.js"></script>
  </head>
  <body>
    <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>    //當點擊此按鈕時,會觸發MyEvent事件,這時會把此事件也傳遞給父做用域,也就是Root scope,這時它的count會增長1.固然同級的Middle scope的count也會加1.
          <button ng-click="$broadcast('MyEvent')">$broadcast('MyEvent')</button>  //當點擊此按鈕時,會觸發MyEvent事件,這時會把此事件傳遞給子做用域,也就是Leaf scope,這時它的count會增長1,固然同級的Middle scope的count也會加1.
          <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>
  </body>
</html>

script.js:

function EventController($scope) {
  $scope.count = 0;
  $scope.$on('MyEvent', function() {  //監聽MyEvent事件
    $scope.count++;
  });
}

做用域的聲明週期

瀏覽器接收到事件後的通常工做流程是執行一個相應的Javascript回調。回調一執行完,瀏覽器就會從新渲染DOM而且從新回到等待事件的狀態。

當瀏覽器調用AngularJS上下文以外的Javascript代碼時,AngularJS是不知道模型的更改的。要正確處理模型的更改,就要使用$apply方法進入AngularJS的執行上下文。只有在$apply方法內執行的模型修改纔會正確地被AngularJS處理。好比,一個指令監聽DOM事件,好比ng-click,它必須在$apply方法中來執行表達式。

執行完表達式以後,$apply會進入$digest階段。在$digest階段,做用域會檢查全部的$watch表達式,並將它們和以前的值比較。這意味着賦值語句,如$scope.username="angular"不會立刻致使$watch被通知,取而代之的是它會等到$digest階段才被通知。這種方式是合理的,由於它將多個模型的更新整合到一個$watch通知裏,而且保證了一個$watch通知期間不會有其餘一樣的$watch執行。

  1. 建立——根做用域是在應用被$injector啓動時建立的。在模板連接階段,有些指令會建立新的子做用域。 
  2. 觀察者註冊——在模板連接階段,指令會在做用域上註冊觀察者。這些觀察者是用來將模型的改變傳遞給DOM的。
  3. 模型變化——爲了正確地觀測到模型變化,你須要而且只能在scope.$apply()中改變他們。(AngularJS的API會隱式地這麼作,因此在控制器或者在$http,$timeout等服務中你不須要額外的調用$apply)。
  4. 變化的觀測——在$apply的最後,AngularJS會在根做用域中執行一個$digest循環,它會將變化傳遞給全部子做用域。在$digest循環中,全部的$watch表達式或者函數都會被檢測,來觀察模型的變化。若是有變化被檢測到了,$watch的監聽回調就會被調用。
  5. 做用域的銷燬——若是子做用域再也不有用了。那麼子做用域的建立者就會負責用scope.$destroy() API來將它銷燬。這會中止$digest再調用此子做用域,而且讓此做用域佔用的內容可以被回收。

在模板編譯階段,編譯器在DOM中匹配指令。指令一般分爲兩種:

  • 觀察型的指令,例如雙花括號表達式{{expression}},會用$watch來註冊一個監聽者。不管表達式何時改變,這類型的指令都會被通知,而且能更新視圖。
  • 監聽者型的指令,好比ng-click,會向DOM註冊一個監聽者。當DOM監聽者觸發,指令會執行相關的表達式而且使用$apply方法更新視圖。

當一個外界事件(好比用戶操做,計時器或者XHR)觸發時,相應的表達式必須在$apply()方法內,並由其相應的做用域調用,這樣全部的監聽者纔會被正確地更新。

大部分狀況下,指令和做用域交互,不會產生新的做用域實例。可是,有些指令,好比ng-controllerng-repeat會建立新的做用域,並關聯到相應的DOM元素上,你可使用angular.element(aDomElement).scope()方法來得到某一個DOM元素相關的做用域。

做用域和控制器在如下幾種狀況下交互:

  • 控制器經過做用域來向模板暴露方法(參考ng-controller
  • 控制器定義裏能改變模型(做用域的屬性)的方法(行爲)
  • 控制器在模型上註冊了觀察者。這些觀察者會在控制器行爲執行後當即被執行

檢測屬性的改變是AngularJS中一項經常使用的操做,因此它應該是高效的。要注意的是,執行檢測的方法不該該包含任何DOM操做,由於在Javascript對象中,DOM獲取要比屬性獲取慢不少不少。

 

 

 

加油!

相關文章
相關標籤/搜索