Angular开发者指南(六)作用域

什么是作用域?
作用域是引用应用程序模型的对象。 它是表达式的执行上下文。 作用域以层次结构排列,模仿应用程序的DOM结构,它可以观察表达式和传播事件。
作用域的特征
Scope提供API($watch)来观察模型改变。
Scope提供API($apply),通过系统将任何模型更改传播到"AngularJS领域"(控制器,服务,AngularJS事件处理程序)外部的视图中。
Scope可以嵌套以限制对应用程序组件的属性的访问,同时提供对共享模型属性的访问。 嵌套的作用域是“子作用域”或“隔离作用域”。 “子作用域”(原型)从其父作用域继承属性。 “隔离作用域”不从父作用域中继承属性。
Scope提供对其评估表达式的上下文。 例如{{username}}表达式没有意义,除非根据定义username属性的特定作用域进行求值。
作用域作为数据模型
Scope是应用程序控制器和视图之间的粘合剂。 在模板链接阶段,指令在作用域上设置$watch表达式。 $watch允许通知属性更改的指令,这允许指令将更新的值呈现给DOM。
控制器和指令都涉及作用域,但不是互相。 这种措施将控制器与指令以及DOM隔离。 这是一个重要的点,因为它使控制器被视为不存在,这极大地改善了应用程序的测试环节。

script.js

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

index.html

<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中,这种行为被称为原型继承,而子作用域原型继承自他们的父母。
此示例说明了应用程序中的作用域,以及属性的原型继承。示例后面是描述作用域边界的图。

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.浏览器使用更新的文本重新呈现视图。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值