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获取要比属性获取慢不少不少。

 

 

 

加油!

相关文章
相关标签/搜索