什么是做用域?
做用域是引用应用程序模型的对象。 它是表达式的执行上下文。 做用域以层次结构排列,模仿应用程序的DOM结构,它能够观察表达式和传播事件。
做用域的特征
Scope提供API($watch)来观察模型改变。
Scope提供API($apply),经过系统将任何模型更改传播到"AngularJS领域"(控制器,服务,AngularJS事件处理程序)外部的视图中。
Scope能够嵌套以限制对应用程序组件的属性的访问,同时提供对共享模型属性的访问。 嵌套的做用域是“子做用域”或“隔离做用域”。 “子做用域”(原型)从其父做用域继承属性。 “隔离做用域”不从父做用域中继承属性。
Scope提供对其评估表达式的上下文。 例如{{username}}表达式没有意义,除非根据定义username属性的特定做用域进行求值。
做用域做为数据模型
Scope是应用程序控制器和视图之间的粘合剂。 在模板连接阶段,指令在做用域上设置$watch表达式。 $watch容许通知属性更改的指令,这容许指令将更新的值呈现给DOM。
控制器和指令都涉及做用域,但不是互相。 这种措施将控制器与指令以及DOM隔离。 这是一个重要的点,由于它使控制器被视为不存在,这极大地改善了应用程序的测试环节。css
script.jshtml
angular.module('scopeExample', []) .controller('MyController', ['$scope', function($scope) { $scope.username = 'World'; $scope.sayHello = function() { $scope.greeting = 'Hello ' + $scope.username + '!'; }; }]);
index.htmlexpress
<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中,这种行为被称为原型继承,而子做用域原型继承自他们的父母。
此示例说明了应用程序中的做用域,以及属性的原型继承。示例后面是描述做用域边界的图。app
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控制。
要检查调试器的做用域:
在浏览器中右键单击感兴趣的元素,而后选择“检查元素”。 应该看到浏览器调试器与你点击的元素突出显示。
调试器容许以$0变量访问控制台中当前选定的元素。
在控制台中执行检索相关联的做用域: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如何与浏览器的事件循环交互。
浏览器的事件循环等待事件到达。事件是用户交互,定时器事件或网络事件(来自服务器的响应)。
事件的回调被执行。这将进入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.浏览器使用更新的文本从新呈现视图。