什么是作用域?

作用域是引用应用程序模型的对象。 它是表达式的执行上下文。 作用域以层次结构排列,模仿应用程序的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类放置在附加了作用域的元素上。 此示例中的定义以红色突出显示新作用域位置。 子作用域是必需的,因为repeater计算{{name}}表达式,但是根据表达式的作用域来计算,它会产生不同的结果。 同样,{{department}}的计算,它的作用域原型从根作用域继承,因为它是唯一定义了department属性的地方。

从DOM中检索作用域

作用域作为$scope数据属性附加到DOM,并且可以检索以用于调试目的。 (这不太可能需要在应用程序内以这种方式检索作用域。)根作用域附加到DOM的位置由ng-app指令的位置定义。 通常,ng-app放置在元素上,但也可以放置在其他元素上,例如,只有一部分视图需要由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指令在控制上设置一个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.浏览器使用更新的文本重新呈现视图。

最新文章

  1. 详解Eclipse断点
  2. Tomcat源码解读系列(一)——server.xml文件的配置
  3. Metaweblog在Android上使用
  4. dipole antenna simulation by CST
  5. POJ 2891 Strange Way to Express Integers【扩展欧几里德】【模线性方程组】
  6. 【面试题】Google of Greater China Test for New Grads of 2014总结
  7. java开发--struts2 标签库使用
  8. Qt之QLabel
  9. 【iOS开发-35】有了ARC内存管理机制,是否还须要操心内存溢出等问题?——面试必备
  10. hdoj1242(bfs+priority_queue)
  11. postman定义公共函数
  12. linux系统管理--进程管理
  13. Spring IOC容器分析(4) -- bean创建获取完整流程
  14. java创建线程的几种方式,了解一下
  15. css实现发光文字,以及一点点js特效
  16. Spring Boot 自定义 starter
  17. PHP中使用CURL之php curl详细解析
  18. 【leetcode】485. Max Consecutive Ones
  19. python模块 os&amp;sys&amp;subprocess&amp;hashlib模块
  20. [troubleshoot][daily][redhat] 设备反复重启故障排查

热门文章

  1. java笔记3-手写
  2. [极客大挑战 2019]BuyFlag
  3. 提高js性能的方法
  4. shell中获取文件目录方法
  5. sudo: /etc/sudoers is mode 0777, should be 0440 单用户 sudo不用输入密码的方法
  6. rename 修改文件名
  7. Python合成GIF图片 -- imageio库
  8. 在storyboard中给控制器添加导航栏控制器和标签控制器
  9. python 运算符 取余 取商 in not in
  10. py02_02:pyc的解释