AngularJS–Scope(作用域)

点击查看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 + '!';
  };
}]);

效果图:

image

在上面的例子当中,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;
}

效果图:

image

image

请注意:当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++;
  });
}]);

效果图:

image

点击这里查看示例

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-controllerng-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一个副本到内存中去。所以,它的性能就得你自己评估是否适当了。建议还是不要深度太大,层级太多,不然性能很差了。

图示如下:

concepts-scope-watch-strategies (1)

和浏览器的事件循环的集成

下面的图表和例子描述了浏览器的事件循环如何和Angular相互作用的。

1、浏览器的事件循环等待事件的到来。一个事件一般是用户的交互触发,定时器事件,或网络事件。

2、事件的回调将会在事件触发时,进入该事件的JavaScript环境进行执行。回调函数可以修改DOM的结构。

3、一旦回调执行,浏览器会离开JavaScript环境,并且会重新渲染修改后DOM到UI界面。

图片描述:

concepts-runtime

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。

原文地址:https://www.cnblogs.com/leosx/p/4898492.html