点击查看AngularJS系列目录
转载请注明出处:http://www.cnblogs.com/leosx/
Scope
Scope 是一个应用程序的模块的对象。它是表达式的执行上下文。它充斥在DOM树的各个层级上。作用域Scope可以监控表达式也可以广播事件(监控表达式,就是WPF中的属性变更通知,相当有作用哟!)。
Scope的特点
Scope有一个监控方法($watch),用它来监视model(模型)的变化,也就是上面所说的监视并做变更通知。
Scope有一个($apply)方法,我们用它就可以去执行一些来自非Angular的代码,或者第三方内库功能。它的好处就是让第三方函数加入了Angular框架,走了AngularJS的生命周期,我们就可以在AngularJS的生命中期中详细的控制了。
Scope也可以嵌套到应用程序限制访问的一些组件中去。并且可以提供一些共享的属性。嵌套进去的Scope是一个“子Scope”或者是一个“独立Scope”。需要注意的是:“子Scope”是继承自它的父级Scope的,它有父级Scope的属性和方法。而“独立Scope”则没有继承父级Scope。要查看更多独立Scope(isolated scopes),请点击链接进行查看。Scope为我们的表达式提供了上下文。例如单纯的{{username}}表达式是没有任何意义的,因为它没有上下文,访问不到username变量。为了使得表达式起作用,我们就要在表达式对应的组件中的Scope上定义一个username属性,并且为它赋值,然后这个表达式就有上下文了,就可以访问到uername属性并且渲染显示。
Scope之VM(ViewModel)
Scope是应用程序(app)的controller和view之间的粘合剂。在模板(template)进行链接(linking)期间,会使用scope的$watch
表达式去监视一些指令所引用的对象,换句话说就是$watch
可以对Scope上的model(ViewModel)进行监控,当model上的属性变化时,就会通知UI进行更新,如果我们自定义了监视。那么同样会调用我们自定义的监视代码。这个就是WPF上的属性变更通知。是相当有用的东东。来一个例子:
第一个文件:index.html
<div ng-controller="MyController"> Your name: <input type="text" ng-model="username"> <button ng-click='sayHello()'>greet</button> <hr> {{greeting}} </div>
第二个文件script.js
angular.module('scopeExample', []) .controller('MyController', ['$scope', function($scope) { $scope.username = 'World'; $scope.sayHello = function() { $scope.greeting = 'Hello ' + $scope.username + '!'; }; }]);
效果图:
在上面的例子当中,MyController
控制器的 username
属性的值是World
。它被ng-model
指令分配到了input
对象上,进行了一个双向绑定,也就是说,当用户在UI界面中,在input中输入数据时,会自动把数据更新到username
属性上,如果在js中,修改username
属性的值,那么同样的,Angular会通知UI进行更新input的显示值。这就是双向绑定。
Scope的继承
每一个AngularJS应用程序有且只有一个根scope(root scope),但是,允许拥有很多个子集scope。
在一个Angular应用程序中,是可以拥有多个scope的,因为有一些指令,是会自动创建scope的(参照指导文件,以查看哪些指令创建新的scope)。当指令自动创建scope的时候,会继承父级scope。也就是拥有上级scope的所有属性和方法。
例如,我们在执行 {{name}}
表达式的时候,首先会在scope中寻找这个name属性,如果找不到,那么会自动去父级scope上找name属性,以此类推,直到rootScope为止。
下面这个例子演示了scope的应用,也是一个原型继承(prototypical inheritance)。例子中,明确标识出了scope的边界。
第一个文件:index.html
<div class="show-scope-demo"> <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 = 'Angular'; }]) .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; }
效果图:
请注意:当scope被附加到了元素上之后,会自动的为这个元素加上ng-scope
样式。这个例子中的<style>
样式用来标识scope边界。
在DOM树上检索scope
scope被附加到DOM上的$scope属性上(在应用程序内,是不可以这样去检索scope的哦!)。其中rootscope会被附加到ng-app
指令所对应的DOM上。通常,ng-app
指令会被附加到<html>
元素上去。也可以被附加到其他的标签上去。
让我们来使用chrome的debugger来介绍下scope。
1、在chrome浏览器中,右击你要查看的元素,在右键菜单中选择【审查元素】,然后你就可以看见这个元素被debugger高亮显示出来了。
2、调试器允许我们在控制台中使用
$0
变量去访问当前选中的元素。3、可以在控制台使用
angular.element($0).scope()
或者$scope
去访问选中元素所在的scope
。
Scope的事件广播
scope可以以类似DOM事件的样子进行广播一个事件。该事件可以被广播到子集scope或者父级scope上去。我们来看一个例子:
文件一: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>
文件二:script.js
angular.module('eventExample', []) .controller('EventController', ['$scope', function($scope) { $scope.count = 0; $scope.$on('MyEvent', function() { $scope.count++; }); }]);
效果图:
Scope的生命周期
正常情况下,浏览器接收到事件触发信息后,会直接执行回调函数。一旦回调完成,浏览器重新渲染DOM并显示出来,也就是更新UI,并且立刻返回,以便等待接收其它的事件触发。
当浏览器在AngularJS的scope(上下文)之外调用JavaScript的时候,AngularJS是无法知道model被修改了,也就无法实现双向绑定了。要想正确的实现model的双向绑定,那么就要把JavaScript使用$apply
方法加入到AngularJS的生命周期当中去,这样AngularJS之外的JS也可以正确的进行双向绑定了。例如,如果一个指令,监听DOM事件,比如:ng-click
它也是在$apply
方法中进行调用的,这样才能正确的响应数据模型(VM-ViewModel)。实现双向绑定。
在表达式计算完毕后,$apply
方法会调用$digest
。在执行$digest
方法的时候,scope会检查所有的$watch
表达式;并将它们与以前的值进行比较。注意:这个值的变更检查是异步方式进行的。这意味着,如果执行了诸如:$scope.username="angular"
的操作将不会立即更新UI;因为$watch
的通知还没有发出来。$watch
的变更通知会被延迟到$digest
执行的时候进行。这个延迟是合理的哈!因为它组合了model的多个属性变更通知到一个$watch
变更通知表单中。这样就可以保证在$watch
的通知表单在执行通知的时候,没有其它的通知也在同时进行。
Scope的生命周期有如下几个阶段:
1、创建阶段 -- 让AngularJS应用程序在启动$injector的期间,就会创建 root scope (唯一的根Scope)。当模板(template)在进行链接的阶段,一些指令会创建子scope(child scope)。
2、监视器的注册 -- 在template(模板)链接期间,指令会通过scope的
$watch
注册监视。这些监视用于将变更广播到DOM上,以进行UI变更。3、model变化 -- 要想model的变化被正确的监视到,你必须把更改model的表达式放在scope.$apply()方法中执行。Angular在这方面做的很精简的,在controller中执行model修改或者在诸如$http, $timeout 或者 $interval等异步服务中修改model的话,都会自动去调用
$apply
方法的,就不再用自己去调用$apply
方法了。4、观察变化 -- 在
$apply
方法结束的时候,Angular会调用rootscope上的$digest方法进入销毁阶段,然后再广播给child scope,告诉他们执行$digest方法,进入他们的销毁阶段。在销毁阶段,所有使用$watch
进行监视的model的表达式或者属性还有方法都会被检查是否有变更,如果有变更,那么久会执行通知。5、scope的销毁 -- 当一个child scope不在需要的时候,那么你就可以调用scope.$destroy() 方法去销毁它们。这个动作将会停止$digest销毁广播的向下传递,并且也允许内存去回收child scope的所使用的内存。
scope和指令
在编译阶段,编译器会去匹配对DOM模板上的指令。这些指令通常分为以下两类:
1、监视类(Observing)指令,例如:双花括号
{{表达式}}
,它使用$watch() 方法去进行监视。这类表达式在表达式变化了的时候,会通知UI进行界面更新。2、监听类(Listener)指令,例如ng-click指令,注册一个对DOM的监听,当DOM事件触发时,会去执行它自己的表达式,并且使用$apply() 方法去更新UI界面。
当接收到一个外部事件(例如用户动作,定时器或XHR),它们的表达式必须在$apply()方法中去执行,以便所有监视者能正确更新数据到所自己所在的scope上。
指令创建Scope
在大多数情况下,directives (指令)和scope会相互影响,但是不会创建出新的scope出来。然而,有一些指令,诸如:ng-controller和 ng-repeat指令, 它们会创建一个child scope然后附加到对应的DOM元素上去。你可以调用angular.element(aDomElement).scope()
方法去取到任何DOM元素身上的scope信息。
控制器(controller)和scope
控制器和scope会在以下几种情况下相互影响:
1、控制器(controller)使用scope来暴露方法和属性给模板(template)使用和访问。
2、controller定义的一些方法(或者行为behavior),可以去改变scope上的属性值。
3、控制器可以为model注册watche 监视,这些监视会在controller的动作加载之后立即启动。
查看更多和ng-controller相关的信息,点击这里。
Scope $watch
的性能注意事项
在Angular中,scope对model属性的变更检查是一个公共的方法。正因如此,变更检查功能必须是有效的。应该注意的是,变更检查并没有去做任何的DOM操作的哦!访问DOM元素会要比访问JavaScript的属性的速度要慢。
scope $watch
的深度
变更检查可以用三种策略来实现:通过引用(reference)、通过集合(collection contents)、通过value。这几种方式的性能是有不同的;而且方式也不一样。
1、通过引用方式(by reference):也就是
scope.$watch(watchExpression, listener)
方法。当检测到变化时,$watch表达式监控的所有值会更新,并且整体返回。需要注意的是,如果我们监视的是一个Array数组或者一个对象时,对象或数组里面的数据变化时,是不会被检测到的。这个策略的性能是最好的。2、通过集合(collection contents)方式:也就是
scope.$watchCollection(watchExpression, listener)
方法;这个方法就弥补了上面一种方式的不足,这种方式会监视到数组或者对象的内部变化。当为一个数组增加,删除或者重新排序时,都会进行变更通知的。不过这种方式并不是嵌套监视的,它只监视被监视集合或者对象下的直接子元素的变化,不会监视子元素的子元素。相比引用方式去监听,这种方式性能上肯定会差一些。但是,某些情况下,使用它是最好的选择。3、通过value方式:也就是
scope.$watch (watchExpression, listener, true)
方法来进行监视,这种方式会监视到被监视对象的所有子元素,无论是间接子元素还是直接子元素,都会被监视。他是监视最全面的,同时也是性能代价最大的。在销毁阶段,它会遍历嵌套的所有数据,并且会copy一个副本到内存中去。所以,它的性能就得你自己评估是否适当了。建议还是不要深度太大,层级太多,不然性能很差了。
图示如下:
和浏览器的事件循环的集成
下面的图表和例子描述了浏览器的事件循环如何和Angular相互作用的。
1、浏览器的事件循环等待事件的到来。一个事件一般是用户的交互触发,定时器事件,或网络事件。
2、事件的回调将会在事件触发时,进入该事件的JavaScript环境进行执行。回调函数可以修改DOM的结构。
3、一旦回调执行,浏览器会离开JavaScript环境,并且会重新渲染修改后DOM到UI界面。
图片描述:
Angular通过提供一个属于自己的事件轮询处理机制去修改了正常的JavaScript流的执行。所以JavaScript的执行环境就分为了正常的JavaScript流环境和Angular事件环境两种情况。只有那些在Angular执行上下文环境中执行的操作,才会具有Angular提供的诸如:数据绑定,异常处理(exception handling),属性监视等功能。你也可以使用$apply()
方法把常规的JavaScript代码加入到Angularjs的执行上下文环境中进行执行,这样我们的JavaScript代码就可以具有上面提到的那些Angular提供的功能了。请记住,在大多数地方诸如:controllers, services等指令中,当事件处理完成后,都会自动为你调用$apply()
方法。也就是你不用自己手动调用$apply()
方法了。 只有我们自定义的JavaScript代码,或者自己直接操作DOM的JavaScript代码,或者第三方类库的回调函数才会需要手动调用$apply()
方法来加入Angular的执行上下文环境中。
$apply()
方法的使用以及执行流程大致有如下几步:
1、通过调用
scope.$apply(stimulusFn)
方法进入Angular的执行上下文环境,其中stimulusFn
是你希望在Angular中执行的工作.2、Angular会执行
stimulusFn()
方法,通常这种工作都会修改应用程序的状态。3、Angular进入轮询($digest loop)。这个轮询中会有两个小的轮询,它们分别是进行$evalAsync队列处理,和执行和$watch 监视相关的工作。$digest 轮询会一直迭代,直到模型(model)保持稳定,也就意味着$evalAsync队列是空的了,并且$watch 监视表单中不再有任何改变。
4、$evalAsync队列用于调度那些不在当前堆栈(我理解为Angular环境)中工作,并且要再浏览器进行渲染之前的工作。比如 ,通常
setTimeout(0)
的调用完成了,但是受到延迟的影响或者那些可能因为在事件执行后,浏览器重新渲染View的时候造成的画面(UI)闪烁的问题的影响的工作,就会被$evalAsync队列调度。5、$watch 的集合是一组在最后一次迭代的时候可能发生了变化的表达式。如果检测到了变化,
$watch
方法就会被调用,这个方法通常是把DOM上对应元素的旧值更新为现在变化后的新值(这时改变了DOM,浏览器并没有重新渲染)。6、一旦$digest 轮询完成了,便会离开Angular的执行上下文。在这之后,浏览器就开始了重新渲染DOM,也就是刷新UI界面了。
下面阐释了当用户在文本框中输入一段Hello world
文字后,是如何实现数据的绑定效果的。
一、在编译阶段:
1、ng-model 指令和input directive 指令会监听
<input>
控件的keydown
事件。2、插值(interpolation)使用$watch 去注册
name
的变更时的通知。
二、在运行阶段:
1、在键盘上按下'
X
' 键,使得浏览器去激活这个<input>
控件的keydown
事件;2、input (点击我,查看有哪些input指令)指令捕获到了输入值的改变,并且调用了$apply
("name = 'X';")
方法去更新Angular执行上下文的模型(model);3、Angular把model上的
name = 'X';
4、开始$digest轮询;
5、$watch 的集合监视到了
name
属性的改变,并且通知了interpolation,从而更新了DOM;6、Angular退出执行上下文,这样就会退出
keydown
事件所在的JavaScript的执行上下文;7、浏览器重新渲染view视图,刷新UI。