翻译:angularJS Scope综述

笔记类文章

angularJS Scope综述

于2017年1月14日 翻译自angularjs 官网开发者指南

每一个应用都仅有一个根域,其他所有域都是它的子孙,scopes将model和view分离开来,并通过一种机制监控model变化,它也提供事件的发散/广播并且提供设施。

一个根域($rootScope),通过$injector加载根域的名字,可以被重新获取。子域(childScopes)通过$new()方法来创建。(大部分的域scope是在HTML模版编译完成时自动创建的)

英文原文

什么是Scope

scope是一个应用模型对象,它是一个表达式的执行上下文。它被用于那些模仿DOM结构的分层结构的应用里。它可以监控表达式和传播事件。

Scope特性

  • Scopes提供($watch)来观察model的变化
  • Scopes提供($apply)来沟通视图(view)到angular领域外的系统,以便传播任何model的变化,(controllers, services, angular event handlers)
  • 可以通过嵌套scope来限制其对应用程序组件属性的使用权。嵌套域是child scopesisolate scopes(独立子域,directive创建的scope)这种子域。child scope会从父域继承属性,isolate scope则不会
  • Scopes会针对被求值的表达式提供上下文,eg:{{username}}表达式是无意义的,除非在一个特定的scope中定义了username

数据模型的scope

  • scope是应用在控制器和视图之间的粘合剂。在模版的linking阶段,directives指令在scope上建立$watch来监控属性的变化(如果变化,通知directive),并允许指令为DOM重新渲染更新后的数据。
  • 所有的控制器和指令都与scope有关,而不是彼此有关,这种布局将控制器与指令很好的分离开来,就像控制器与DOM一样。让控制器变的不可知是很重要的,这大大改善了应用调试时的情况。
  • 在逻辑上,渲染dom中如{{greeting}}过程是:
    • 遍历scope关联的模版中被定义{{greeting}}的DOM节点。
    • 依据正在遍历的scope重新计算表达式,然后重新设置结果。
  • 可以把scope和其上属性想象为用于渲染视图的data。scope仅仅是所有与视图有关的实物的实际来源(source-of-truth)。
  • 在一个视图的可测试点(testability point of view?可能是angular的测试模块的东西ngMock),分离视图和控制器是不可能的。因为它允许我们检测行为而不用分心于渲染细节。

scope 层级

  • 每个angular程序都只有一个root scope,但有很多的孩子域。
  • 应用可以有多个域,因为有一些指令会创建新的子域,新的子域在创建完成后,被当作父域的孩子插入到父域中。这种方式,将在与Dom互相依赖的地方,创建了一颗与dom平行的树状结构。
  • 来看一个例子:
//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>
//javascript
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'];
}]);

域的划分情况:

.1484355221750.png
  • 应当注意:angular自动添加ng-scope类名到那些被附加了scope的元素上。
  • 子域是必要的,因为重复的对如{{name}}这样的表达式求值,这时,根据表达式求值时的子域不同,得到不同的结果。类似的,对于{{department}}的求值,他继承自根域。

从DOM retrieving(重新检索?) scopes

  • scope以$scope这样的data属性附加到DOM上,处于检错的目的,他们是可以被检索的。(这不是说,将必须在程序中以这种方式重新检索scope)
  • ng-app指令定义了root scope将被附加到DOM的哪个位置。对于将ng-app放在<html>标签上,如果一个页面只有一部分需要被angular控制,那么放在其他的位置会更好。
  • 在debugger中检查scope
    • Right click on the element of interest in your browser and select 'inspect element'. You should see the browser debugger with the element you clicked on highlighted
    • debugger允许在控制台中以$0变量来使用当前选择的元素
    • 通过angular.element($0).scope()来在控制台重新检索元素关联的scope,或者输入$scope也可以。

域事件的传播

  • 在同样的fashion中,scope可以向dom事件传播事件,事件可以broadcasted(广播)给孩子域或emit(发散)给父域。
  • 使用$emit('事件名')来发散给父域,$broadcast('事件名')广播给子域。
  • 在接受事件的域使用$on('对应的事件名')来接受(注意,两边的事件名必须一致)
  • eg:
//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>
angular.module('eventExample', [])
.controller('EventController', ['$scope', function($scope) {
  $scope.count = 0;
  $scope.$on('MyEvent', function() {
    $scope.count++;
  });
}]);

scope生命周期

  • 浏览器普通流在一个事件执行相应js回调函数时才接受它。一旦回调函数执行完毕,浏览器会重新渲染dom,并返回到等待事件的状态。
  • 当浏览器调用angular执行上下文之外的js代码时,这意味着angular不会意识到model的修改。为了正确的处理model的改变,需要使用$apply方法,将执行过程加入到angular的执行上下文。只有在$spply中的model处理,才将被angular正确的解释。比如,一个指令监听dom事件,他必须在$apply方法中对表达式求值。
  • 在对表达式求值后,$apply方法将执行$digest.在$digest阶段,scope检查()所有的$watch表达式,并且将他们与以前的值进行比较。这个就是脏检查,它是异步的。这意味着,赋值将(eg:$scope.username = "angular")不会立刻通知$watch,而是延迟到$digest阶段。这种延迟是必要的,因为,它会在一个$watch中合并大量的model更新,同时,也保证了,在这个$watch执行的过程中,没有别的$watch正在运行。如果在某个$watch中又改变了model的值,他会强制触发额外的$digest循环
    • Creation
      root scope在应用$injector的引导程序(bootstrap)的过程中被创建,在连接模版(linking)时,一些指令会创建子域。
    • Watcher registration (注册观察者)
      在模版连接(linking)时,指令(directive)将在域(scope)上注册观察者(watches),这些观察者将被用于传播model的值到DOM
    • model mutation(变化)
      因为变化需要被正确的观察到,应确保他们仅仅在scope.$apply()内部。angular APIs,隐含的做了这样的处理,所以,在controllers中进行同步的任务时;或者使用$http,$timeout,$interval等服务进行异步任务时,不需要额外的调用$apply.
    • mutation observation(变化观察)
      所有的$apply的末尾,angular会在root scope上执行一个$digest循环,这个循环会波及到所有的孩子域。在$digest期间,所有被$watch的表达式或者函数,会被检查model是否变化,一旦发现变化,$watch的监听者就被调用。
    • scope destruction(域的消亡)
      当某个子域(child scope)不再不需要了,那么这个子域的创造者有义务通过scope.$destroy()api来销毁这个子域。这将停止再向这个子域传播$digest调用,并且允许,被这个子域model占用的内存被garbage collector回收。

scopes 和 directives(域和指令)

在编译阶段(comlilation),compiler编译器会针对DOM模版来匹配directives指令,指令通常分为一或两种类别:

  • 观察者型指令(observing directives),比如两个花括号的表达式{{expression}},它会通过$watch()方法注册监听者。这种类型的指令,只要表达式变化(值的变化)就会被通知,所以它可以更新视图数据。
  • 监听者型指令(listener directives),比如ng-click,在DOM上注册一个监听者,当这个dom监听者激发时,这种指令会执行与其相关联的表达式并且通过$apply()方法来更新视图(view)
    当接收一个外部的事件时,它关联的表达式必须通过$apply()方法应用到scope,以保证所有的监听者正确的更新。

创造域的指令

在大多数情况下,指令和域将继承而不是创造,一个新的scope实例。然而,有的指令,比如ng-controller,ng-repeat会创造一个新的子域并且将这个子域关联到相应的DOM元素上,可以在任何DOM元素上,通过angular.element(aDomElement).scope()方法来检索scope。

控制器和域

域和控制器在以下情况将会互相影响

  • 控制器通过scope暴露控制器的方法
  • 控制器定义可以影响model(scope上的属性)的方法时。
  • 控制器可能会在model上注册watches,这些watches会在控制器行为执行后,立刻执行。

scope watch性能注意事项

脏检查scope属性的变化,在angularz中是一个公共的操作,所以脏检查函数需要很高效。需要注意,在脏检查函数中,避免过多的DOM操作,因为,DOM操作比在js对象上操作属性慢的多的。

Scope$watchdepths 深度域$watch

如图:

.1484375972331.png

完成脏检查有三种策略,引用(by reference),数据集合(by collection items),值(by value)。这些策略的不通点在于他们察觉的变化的种类,和他们各自的工作特性(运行方式)。

  • 通过引用监听(scope.$watch(watchExpression, listener)),当监听表达式转变为一个新的值并返回完整的值时,可以察觉到变化。如果返回值是一个array或者object,那么他们内部的变化将不会被察觉到,这是最有效率的模式
  • 观察数据集合 (scope.$watchCollection(watchExpression, listener)),会察觉到在数组或对象内部的变化:当item被添加,移除或者重新排序。它是一个浅观测——即,不会观测到嵌套结构下的数据集合(就是对象内嵌对象查不到)。这种策略比上一种代价高,因为需要维持一个数据集合的副本。但这个策略也会企图使复制请求的总数尽量的少。
  • value监听($scope.$watch(watchExpression, listener, true)),察觉到所有的变化,(各种嵌套数据结构之类的,都会检查),这是最全面的检查变化策略,但是也是代价最大的,在每一个digest都必须完全遍历嵌套的数据结构,并且在内存中维持一个完全拷贝的的副本

集成浏览器事件循环

.1484379812975.png

上图和接下来的例子将会描述,angular是如何集成浏览器的事件循环

  • 浏览器的事件循环等待一个事件的到达。(用户交互事件,timer事件,网络任务事件)
  • 某事件的回调函数在js上下文中得到执行,这个回调函数可以修改dom结构。
  • 一旦回调函数得到执行,浏览器就离开javascript上下文,并且根据dom的变化重绘视图(view)。

angular通过提供它自己的事件执行循环,修改了普通javascript流,它将javascript分成了普通上下文和angular执行上下文,只有应用在angular执行上下文才能具有angular的数据绑定,异常处理,属性监控,等等。当然,也可以使用$apply()方法从javascript进入angular执行环境。要记住,在很多地方(controllers,services)$apply已经被那些正在处理事件的指令调用过了。仅仅在执行自定义事件回调函数或者需要运行第三方库的回调函数的时候使用$apply()

  • 通过调用scope.$apply(stimulusfn)来进入angular执行上下文,stimulusfn是你希望在angular执行上下文中运行的任务
  • angular运行stimulusFn()来修改应用的状态。
  • angular进入$digest循环,这个循环是建立在两个更小的循环之上的:分别用于处理$evalAsync队列和$watch列表。$digest将会持续迭代,直到model稳定——即$evalAsync队列为空且$watch列表不检查任何变化。
  • $evalAsync队列常用于安排那些需要在当前堆栈外,但在浏览器更新视图前发生的任务,通常使用setTimeout(0)实现,但是,setTimeout(0)方法很迟钝并且可能导致视图闪烁,因为浏览器会在任何一个事件后渲染视图。
  • $watch是一个列表,保存着由于最近一次迭代而发生改变的表达式们。如果一个改变被检查到,那么就调用那些以新值更新DOM的$watch函数。
  • 一旦angular的$digest循环完成,执行上下文将离开angular和javascript。接下来浏览器重构DOM来反映出所有的变化。

下面解释一下hello word例子中,当用户输入文本时是如何实现数据绑定的效果的。

  • 在编译阶段
    • ng-modelinput directive<input>建立一个keydown监听者
    • interpolation(插入者?处理器?)建立一个$watch以便name变化时通知它。
  • 在运行阶段
    • 按下某个键盘键x,会导致浏览器在<input>控制器上发散一个keydown事件
    • input指令捕获到输入值的变化,并调用$apply("name = 'x';")方法来在angular执行上下文中更新应用model
    • angular将name = "x"应用到model
    • $digest循环开始
    • $watch列表检查到在name属性上有一个变化,并且通知interpolationinterpolation轮流更新DOM
    • angular退出那些,连带javascript执行环境一起轮流退出keydown事件的,执行上下文。
    • 浏览器以新值重新渲染view

继承性

一个域可以从父域继承

在测试scope的相互作用时,为scope的实例添加一些额外的帮助函数是很有效的,这些函数在ngMock中有介绍

  • expect(exp).toEqual(value)
    计算exp并与后边的value做比较

推荐阅读更多精彩内容