什么是做用域?html
Angular中做用域(scope)是模板以及工做的上下文环境,做用域中存放了应用模型和视图相关的回调行为。做用域是层次化结构的与相关联的DOM结构相对应。做用域能够观察表达式以及传播事件。前端
原文: scope is an object that refers to the application model. It is an execution context for expressions. Scopes are arranged in hierarchical structure which mimic the DOM structure of the application. Scopes can watch expressions and propagate events.angularjs
做用域的特性ajax
做用域提供了相关的APIs($watch)来监控模型的状态而且将Angular系统(视图、服务、事件处理器)内部的模型的变化同步到视图。express
做用域能够嵌套来控制应用组件对模型属性的访问。嵌套的做用域能够是“父子”关系或者"同级"关系。子做用域能够继承父做用域的属性,相邻做用域是互补可见的。api
做用域提供了表达式的上下文环境。例如表达式{{username}}只有在定义了username属性的做用域中才有意义。数组
做用域做为数据模型浏览器
做用域是链接Angular控制器和视图的中间地带。指令会在模板连接阶段(linking)在做用域中创建对表达式的监控($watch)服务。这样$watch就能够将模型属性的变化状况及时通知给指令从而更新视图。网络
控制器和指令只能经过做用域链接,不能够直接关联。这样就实现了控制器和视图的解耦。这样就能够实现一套模型绑定多个视图,也提升了前端代码的可测性。数据结构
index.html
script.js
1 angular.module('scopeExample', [])
2 .controller('MyController', ['$scope', function($scope) {
3 $scope.username = 'World';
5 $scope.sayHello = function() {
6 $scope.greeting = 'Hello ' + $scope.username + '!';
7 };
8 }]);
上述例子说明了做用域的工做原理:
1. 在MyController控制器中定义了username属性, 并在输入文本控件中绑定了该属性。username被初始化为'World',这样做用域会通知文本框中并在文本框预填入初始值。
2. 一样控制器在做用域中定义了sayHello行为,并经过ng-click注册到按钮的点击事件。当用户在input中输入其余值时,会经过做用域更新username属性,从而改变sayHello的结果。
运行结果:
{{greeting}}表达式的工做原理以下:
1. 先找到{{greeting}}表达式所在DOM相关的做用域。在此例中为MyController中的$scope.
2. 在做用域中找到greeting属性并替换{{greeting}}, 既而更新了视图。
scope及其属性提供了用来展示视图的数据。(原文: The scope is the single source-of-truth for all things view related.)
从测试的角度考虑, 视图与控制器分离是必要的, 这样咱们就能够单独测试视图后面的行为而不用考虑视图的细节。
1 it('should say hello', function() { 2 var scopeMock = {}; 3 var cntl = new MyController(scopeMock); 5 // Assert that username is pre-filled 6 expect(scopeMock.username).toEqual('World'); 8 // Assert that we read new username and greet 9 scopeMock.username = 'angular'; 10 scopeMock.sayHello(); 11 expect(scopeMock.greeting).toEqual('Hello angular!'); 12 });
做用域的层次化结构
每一个Angular应用都有一个根做用域(root scope), 在根做用域下能够有多个子做用域。一些指令(directives)也会建立子做用域。 新的做用域会被添加到相应的父做用域上,这样就造成了与DOM视图相平行的树形结构。
让咱们经过一个具体的例子来理解:
1 <div class="show-scope-demo"> 2 <div ng-controller="GreetController"> 3 Hello {{name}}! 4 </div> 5 <div ng-controller="ListController"> 6 <ol> 7 <li ng-repeat="name in names">{{name}} from {{department}}</li> 8 </ol> 9 </div> 10 </div>
输出:
对应的DOM结构:
咱们能够注意到Angular会自动给绑定做用域的DOM元素加上"ng-close"类, CSS文件中给ng-scope类的元素加了高亮显示。为<li>建立子做用域是必要的,由于每一个<li>都会有{{name}}表达式指向本身的name属性。{{department}}中department则继承根做用域$rootScope.department属性。
获取DOM的做用域
做用域是与视图相关联的,咱们能够在debug的时候经过api获取绑定到视图的做用域。根做用域(root scope)定义在含有ng-app指令的DOM元素上。一般ng-app放在<html>元素中, 固然ng-app也能够放在任何DOM元素上,例如咱们只想局部视图被angular控制。
在Chrome中咱们只需右击而后选择“审查元素”选项进入调试界面。
经过$0即可得到当前选中的DOM元素。
经过angular.element($0).scope()或者$scope能够得到当前元素对应的做用域。
做用域事件传播
相似DOM事件,咱们能够在做用域间传播事件。事件能够被广播($broadcast)到子做用域或者向上传播到父做用($emit)域中。
让咱们看具体的例子:
1 <div ng-controller="EventController"> 2 Root scope <tt>MyEvent</tt> count: {{count}} 3 <ul> 4 <li ng-repeat="i in [1]" ng-controller="EventController"> 5 <button ng-click="$emit('MyEvent')">$emit('MyEvent')</button> 6 <button ng-click="$broadcast('MyEvent')">$broadcast('MyEvent')</button> 7 <br> 8 Middle scope <tt>MyEvent</tt> count: {{count}} 9 <ul> 10 <li ng-repeat="item in [1, 2]" ng-controller="EventController"> 11 Leaf scope <tt>MyEvent</tt> count: {{count}} 12 </li> 13 </ul> 14 </li> 15 </ul> 16 </div>
1 angular.module('eventExample', [])
2 .controller('EventController', ['$scope', function($scope) {
3 $scope.count = 0;
4 $scope.$on('MyEvent', function() {
5 $scope.count++;
6 });
7 }]);
8
在HTML模板中,咱们经过ng-click注册了点击事件监听,分别来向子做用域广播和向上传播MyEvent时间,在控制器咱们经过$scope.$on('MyEvent')监听事件。
运行结果以下:
做用域的生命周期
一般浏览器接收一个事件时会执行一段Javascript回调执行相关的处理,等回调执行完会自动更新DOM继续等待处理新事件。当浏览器调用的js代码不在angular执行上下文中,angular将不会注意模型的更改。咱们能够经过$apply方法,让模型的更改在angular的上下文中进行。也就是说只有在$apply过程当中的模型更改才会被angular甄别。例如,某个指令监听DOM事件(好比ng-click),必须在$apply方法中执行angular表达式。
在表达式执行完成后,$apply会执行$digest。在$digest过程当中,做用域会检查全部$watch的表达式,比较表达式的当前状态和上一次状态。这种脏数据(dirty data)的检查是异步的。也就是说给模型属性赋予新的值时,$watch不会当即被通知,通知$watch发生在$digest阶段。这个短暂的延迟是有缘由的,angular会批量通知$watch模型的状态状况,而且保证同时只有一个$watch在进行。若是$watch改变了模型的状态,会再强行触发一次$digest过程:
1. 建立 - 更做用域会在应用启动时经过注入器建立并注入。在模板链接阶段,一些指令会建立本身的做用域。
2. 注册观察者 - 在模板链接阶段,将会注册做用域的监听器。这也监听器被用来识别模型状态改变并更新视图。
3. 模型状态改变 - 更新模型状态必须发生在scope.$apply方法中才会被观察到。Angular框架封装了$apply过程,无需咱们操心。
4. 观察模型状态 - 在$apply结束阶段,angular会从根做用域执行$digest过程并扩散到子做用域。在这个过程当中被观察的表达式或方法会检查模型状态是否变动及执行更新。
5. 销毁做用域
当再也不须要子做用域时,经过scope.$destroy()销毁做用域,回收资源。
做用域和指令
在编译(compiling)阶段,angular编译器会将DOM模板和指令匹配绑定。指令通常可分为如下两类:
1. 观察指令(Observing directives), 例如表达式, 经过$watch方法注册监听。这类指令在表达式变化时会被通知从而更新视图。
2. 监听器指令(Listener directives), 例如ng-click会在DOM元素上注册监听事件。DOM事件会触发指令执行相关的表达式或者经过$apply更新视图。
当接收到外部事件时(用户动做,计时器或者ajax相关的事件),相关做用域的表达式必须经过$apply方法执行,确保全部监听器状态被正确的更新。
哪些指令会建立做用域?
在大多数状况下,指令不会自行建立本身的做用域。但一些指令,例如ng-controller, ng-repeat等会建立子做用域和DOM绑定。咱们能够经过angular.element(aDomElement).scope()
获取和DOM元素相关联的做用域。
控制器和做用域
控制器和做用域能够经过如下方式交互:
1. 控制器经过做用域暴露给模板相关的行为。
2. 控制器定义能够操做模型的方法。
3. 控制器能够经过$watch注册监听模型的状态。这些watch会当即在控制器方法执行后被触发执行。
angular会自动检查做用域内模型状态的变动,这些检查并不触及DOM操做,而只是检查做用域的属性。
出于对性能的考虑,对不一样数据类型(引用、集合、 值)的检查会有不一样的策略(参考图中的决策树):
检查引用 - 当表达式返回一个新的对象或者数组时,scope.$watch(watchExpression, listener)不会再具体对象里面的具体内容,而是比较引用是否指向新的内存地址。
检查集合的内容 - 当对集合类型的数据增长、删除元素或者排序时,$scope.$watchCollection(watchExpression, listener)会检查集合中的元素。对集合中内容的检查是Shallow的, 即不会检查嵌套在集合中的集合或者对象。这种策略试图减小对内存资源的消耗。
检查值 - 根据模型的数据结构,scope.$watch (watchExpression, listener, true)
深度遍历数据结构中的每一个域的值,这种策略是最强大的但须要至关大的资源开销。
与浏览器事件的交互
咱们将结合下图分析angular与浏览器事件交互的工做原理:
1. 浏览器的事件环路会一直等待事件发生,这里的事件包括用户的交互,计时器,网络事件等。
2. 事件会触发监听器在js上下文环境中调用相关回调方法更新DOM结构。
3. 回调结束后,浏览器会脱离js上下文环境,基于DOM的变更从新渲染视图。
angular在以上的Javascript流程中加入了本身的事件处理机制,把js分红了传统的js上下文和angular执行的上下文。angular中的操做会得益于angular的数据绑定机制、异常处理和属性监听等框架特性。咱们也能够经过$apply进入angular的执行上下文环境。
在大多数状况下(控制器、服务),angular会自动给咱们调用$apply处理事件。除非是实现自定义的事件及回调或者是与第三方库结合使用时才会显示调用$apply进入angular上下文环境。$apply工做步骤以下:
1. 经过调用scope.$apply(stimulusFn)进入angular上下文环境,stimulusFn为但愿在angular上下文中执行的代码。
2. 一般stimulusFn用来改变应用的状态。
3. angular进入$digest环路,$digest环路包括两个子循环分别处理$evalAsync队列和$watch列表。$digest会持续迭代直到$evalAsync队列清空而且$watch列表中没有任何状态更新。
4. $evalAsync队列被用来调度图中右半部分渲染DOM以前的子任务。
5. $watch列表中包含了上一次迭代后变化的表达式。当相关的模型变化是,$watch会更新表达式的值并更新视图。
6. 当$digest循环终止即离开Angular和Javascript的上下文环境,浏览器会随之更新视图。
基于上述原理咱们详解一下"Hello World"例子中的数据绑定是怎么实现的:
1. 在angular编译阶段:
- ng-model和input指令在<input>控件中创建了"keydown"事件的监听器
- angular经过(interpolation)创建$watch对name属性的跟踪
2. 在运行阶段:
- 键入’x‘ 即触发keydown事件, input指令会捕获输入的变化调用$apply("name = 'x'"),更新模型数据。
- 更新完成后进入$digest循环,$watch列表检测到name属性发生了变化并通知interpolation,更新DOM视图。
- angular退出运行上下文,从而退出了keydown事件和与之相关的js上下文环境。
- 浏览器检测到DOM变化从新展示视图。