什么是作用域?
作用域是引用应用程序模型的对象。 它是表达式的执行上下文。 作用域以层次结构排列,模仿应用程序的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','$rootScope',function($scope,$rootScope) { $scope.name = 'World'; $rootScope.department = 'AngularJS'; }]) .controller('ListController',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控制。
要检查调试器的作用域:
在浏览器中右键单击感兴趣的元素,然后选择“检查元素”。 应该看到浏览器调试器与你点击的元素突出显示。
调试器允许以$0变量访问控制台中当前选定的元素。
在控制台中执行检索相关联的作用域:angular.element($0).scope(),
scope()函数仅在$compileProvider.debugInfoEnabled()为true(这是默认值)时可用。
作用域事件传播
作用域可以以类似的方式将事件传播到DOM事件。 事件可以广播到当前以及子作用域或发射到当前以及父作用域。
angular.module('eventExample',[]) .controller('EventController',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的应用程序引导期间创建。在模板链接期间,一些指令创建新的子作用域。模型改变
为了正确观察改变,你应该使它们只在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性能注意事项
脏检查更改作用域上的属性是AngularJS中的常见操作,因此脏检查函数必须有效。 应该注意脏检查函数不要做任何DOM访问,因为DOM访问比JavaScript对象的属性访问慢几个数量级。
作用域$watch延伸
可以使用三种策略进行脏检查:通过引用,按集合内容和按值。 策略在它们检测到的变化的种类和它们的性能特征方面不同。
通过引用观察(scope.$watch(watchExpression,listener)当watch表达式返回的整个值切换到新值时检测到更改。 如果值是数组或对象,则不会检测到其中的更改。 这是最有效的策略。
观察集合内容(scope.$watchCollection(watchExpression,listener))检测在数组或对象内发生的更改:添加,删除或重新排序项目时。 检测很浅 - 它不能到达嵌套集合。 观察集合内容比通过引用观察更昂贵,因为集合内容的副本需要维护。 但是,该策略尝试最小化所需的复制量。
按值观察(scope.$watch(watchExpression,listener,true))检测任意嵌套数据结构中的任何变化。 它是最强大的变化检测策略,但也是最昂贵的。 每个摘要都需要完全遍历嵌套数据结构,并且需要在内存中保存它的完整副本。
下面是示例的图例
与浏览器事件循环集成
下面的图和下面的例子描述了AngularJS如何与浏览器的事件循环交互。
浏览器的事件循环等待事件到达。事件是用户交互,定时器事件或网络事件(来自服务器的响应)。
事件的回调被执行。这将进入JavaScript上下文。回调可以修改DOM结构。
一旦回调执行,浏览器将保留JavaScript上下文并根据DOM更改重新呈现视图。
AngularJS通过提供自己的事件处理循环来修改正常的JavaScript流程。这将JavaScript分为经典和AngularJS执行上下文。只有在AngularJS执行上下文中应用的操作才能受益于AngularJS数据绑定,异常处理,属性监视等等。还可以使用$apply()从JavaScript中输入AngularJS执行上下文。请记住,在大多数地方(控制器,服务)$apply已经由处理事件的指令调用。只有在实现自定义事件回调或使用第三方库回调时,才需要显式调用 $apply。
通过调用scope.$apply(stimulusFn)进入AngularJS执行上下文,其中stimulusFn是希望在AngularJS执行上下文中执行的工作。
AngularJS执行stimulusFn(),它通常修改应用程序状态。
AngularJS进入$digest循环。循环由两个较小的循环组成,它们处理$evalAsync队列和$watch列表。 $digest循环继续迭代,直到模型稳定,这意味着$evalAsync队列为空,并且$watch列表未检测到任何更改。
$evalAsync队列用于调度需要在当前堆栈帧之外发生的工作,但在浏览器的视图呈现之前。这通常是通过setTimeout(0)完成的,但是setTimeout(0)方法会受到缓慢的影响,并且可能会导致视图闪烁,因为浏览器在每个事件后呈现视图。
$watch列表是一组自上次迭代以来可能已更改的表达式。如果检测到更改,则调用$watch函数,通常使用新值更新DOM。
一旦AngularJS的$digest循环完成,执行离开AngularJS和JavaScript上下文。随后浏览器重新呈现DOM以反映任何更改。
以下是当用户在文本字段中输入文本时,Hello world示例如何实现数据绑定效果的说明。
在编译阶段:
1.ng-model和input指令在<input>控制上设置一个keydown监听器。
2.插值设置一个$watch以通知名称更改。在运行时阶段: 1.按"X"键使浏览器在输入控件上发出按键事件。 2.输入指令捕获对输入值的更改,并调用$apply("name ='X';")来更新AngularJS执行上下文中的应用程序模型。 3.AngularJS应用"name ='X';到模型。 4.$digest循环开始 5.$watch列表检测name属性的更改,并通知插值,这反过来更新DOM。 6.AngularJS退出执行上下文,这反过来退出keydown事件和它的JavaScript执行上下文。 7.浏览器使用更新的文本重新呈现视图。