Angular 中的数据绑定是自动从模型和视图间同步数据,Angular的这种数据绑定实现让你可以将应用中的模型和视图的数据看作一个源, 视图在任何时候都是对模型的一个投影,当模型发生变化,相关的视图也会发生变化,反之亦然。
首先,模板通过浏览器编译, 这个编译步骤生成一个实时视图.对该视图所做的任何更改会立即反映在模型,在模型的任何更改都会传播到视图. 这个模型是单源应用状态(The model is the single-source-of-truth for the application state),大大简化了开发人员的编程模型.你可以把视图简单的想成是模型的投影。
由于视图只是一个模型的投影,它将控制器和视图完全隔开,不需要关注视图. 这样的隔离让Controller没有dom和浏览器的依赖,更加容易测试。
什么是作用域?
作用域是一个对象引用着应用的模型,它是表达式的运行上下文环境。作用域的层级结构模拟应用中dom的层级结构;作用域能够监视表达式和事件传播。
作用域的特点:
- 作用域提供$watch接口监测模型的变化
- 作用域提供$apply接口传播angular体系外的任何的模型变化
- 作用域可以是嵌套的限制访问应用组件的属性,同时提供共享模型的属性。嵌套的作用域可以是子作用域或者是隔离作用域。一个子作用域继承父作用域的属性,一个隔离作用户则不会继承;查看隔离作用域的更多信息;
- 作用域为表达式求值提供上下文。举个例子{{username}}表达式是毫无意义的,除非它求值前指定了特定包含username属性的作用域;
作用域下的数据模型:
作用域是控制器和视图之间的胶水。在模板linking阶段,指令配置watch表达式在作用域中;watch允许指令通知属性的变化,也允许指令渲染更新后的值到dom。
控制器和指令都有作用域的引用,但并不是彼此引用。这项安排就从指令(就是DOM)隔离了控制器。这是一个重要的点,因为它使得控制器不用知道将要如何显示,大大的提升了测试的环境;
代码语言:javascript复制angular.module('scopeExample', [])
.controller('MyController', ['$scope',
function($scope) {
$scope.username = 'World';
$scope.sayHello = function() {
$scope.greeting = 'Hello ' $scope.username '!';
};
}
]);
代码语言:javascript复制<div ng-controller="MyController">
Your name:
<input type="text" ng-model="username">
<button ng-click='sayHello()'>greet</button>
<hr>{{greeting}}
</div>
例子:http://www.fengyunhe.com/docs/angular/examples/example-example41/index.html
在上面的例子中注意到MyController指定World到作用域的username属性。作用域通知相关联的input,然后呈现出已经赋值的input,演示了控制器如何将数据写入到作用域中。
相似的方式,控制器可以定义行为到作用域中,这里是sayHello方法,当用户点击greet按钮的时候将会执行;sayHello方法可以读取username属性并且创建greeting属性。这里演示了作用域中的绑定到html input 组件上的属性会自动更新。
渲染{{greeting}}的逻辑包括:
- 获取与模板上{{greeting}}相关的作用域。在这个例子中,这是与Controller相同的作用域;(我们后面将讨论作用域的层级关系)
- 上一步取到作用域为执行环境,计算greeting表达式的值,并且计算结果设置到到dom元素;
你可以认为作用域和它的属性里的数据用于渲染这个视图;这个作用域是视图上所有相关事物的来源;
从可测试的角度来看,这种分割控制器和视图是不错的,因为它允许我们测试行为缺不需要分心关心渲染细节;
代码语言:javascript复制it('should say hello', function() {
var scopeMock = {};
var cntl = new MyController(scopeMock);
// Assert that username is pre-filled
expect(scopeMock.username).toEqual('World');
// Assert that we read new username and greet
scopeMock.username = 'angular';
scopeMock.sayHello();
expect(scopeMock.greeting).toEqual('Hello angular!');
});
作用域的层级结构:
每个Angular应用都只有一个root作用域,但是可能有多个子作用域;
每个应用有多个作用域,因为一些指令会创建子作用域(refer to directive documentation to see which directives create new scopes)。当一个新的作用域创建后,它将添加到它的父作用域下成为一个子作用域。创建的树形结构平行于dom的结构;
当angular计算{{name}}时,它首先去作用域查看name属性,如果没有找到,就从父级的作用域寻找,一直到root作用域。在javascript这种行为被称为原型继承,子作用域是从他的父级原型继承;
这个例子演示作用域在应用,属性的原型继承。这个例子后面是描绘结果的作用域边界的图;
代码语言:javascript复制<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'];
}
]);
例子:http://www.fengyunhe.com/docs/angular/examples/example-example42/index.html
注意Angular自动放置ng-scope class到作用域相关联的HTML元素上。在这个例子中,样式定义了红色高亮的区域为socpe的区域,子作用域是必须的,因为repeater需要计算{{name}},但是依赖于不同的作用域,最后结果也不同,类似的,计算{{department}} 继承子根作用域,因为只有那一个地方定义了department。
从DOM获取到作用域:
作用域附在dom元素的$scope属性上,可以获取用来做debug的目的,它不太可能在应用中使用。根作用域被附在有ng-app指令的dom元素上。通常ng-app在html元素上,但是它也可以放到其他的元素上,比如页面上只有一部分是用angular来控制的这种情况。
debug情况检查作用域:
- 右键点击元素,选择inspect element,你将看到浏览器调试器中高亮元素;
- 调试器允许你在控制台用$0变量,去访问当前选中元素。
- 在控制台中获取当前元素所在的作用域,需要执行:angular.element(0).scope() or just type scope
作用域内的事件传播:
作用域可以类似dom事件一样的传播事件,事件可以广播到作用域的子作用域或者是发到上层的作用域;
代码语言:javascript复制angular.module('eventExample', [])
.controller('EventController', ['$scope',
function($scope) {
$scope.count = 0;
$scope.$on('MyEvent', function() {
$scope.count ;
});
}
]);
代码语言:javascript复制<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>
例子:http://www.fengyunhe.com/docs/angular/examples/example-example43/index.html
作用域的生命周期:
浏览器接收到事件后的一般流程是执行对应的js回调函数。当回调执行完成后,浏览器重新渲染dom,然后返回继续等待更多的事件。
当浏览器调用的js代码不在angular执行上下文时,意味着angular无法发现模型的修改。要正确的处理模型修改,执行就要在angular执行上下文中使用apply方法。只有模型修改的执行在apply方法才能正确的被angular理解,举例,如果一个指令监听dom事件,比如ng-click,它必须计算表达式在
表达式计算后,apply方法执行digest.在digest过程中上下文监测所有watch表达式并对比原来的值。这个脏检查是异步完成的。这说明分配动作像scope.username=”angular”将不会立即发生一个watch通知,而是watch的通知将延迟一直到digest阶段。这个延迟是必要的,因为它收集多个模型的更新到一次watch通知中,保证在watch通知时没有其他的watch已经在运行。如果watch修改了模型中的值,将会触发一次
- Creation / 创建 根作用域在应用启动的时候由$injector创建,在template linking阶段和指令时将会创建新的子作用域;
- Watcher registration / 注册watcher 在template linking 指令注册注册watches到作用域中。这个watches将用于填充模型中的值到dom上。
- Model mutation / 模型变动 要想正确的观察到变化,你应该只在scope.apply中使用他们。(Angular APIs对这个操作是隐含的,所以在调用同步的任务不必刻意去调用apply,异步的工作例如http,timeout,
- Mutation observation / 变动的诊断处理 在apply的最后,angular执行一个digest周期使用根作用域,同时将会填充所有的子作用域。在digest周期中,所有watch 表达式或方法将会检查变化,检查到后,
- Scope destruction / 销毁 当子作用域不在需要的时候,子作用域创建者通过作用域的destroy()API 去销毁。这将停止传播digest调用到子作用域、并且允许内存通过使用子作用域模块去被垃圾回收器给回收。
作用域和指令:
在编译阶段,编译器从DOM模板中匹配指令,指令通常分为两类:
- 观察指令,例如双大括号表达式,注册监听器使用$watch方法。这种类型的指令在表达式发生变化的时候会被通知用来更新视图。
- 监听指令,像是ng-click,注册一个监听器在dom上。当dom的监听器触发后,这个指令将执行相关的表达式并且更新视图使用$apply方法。
当接收到一个扩展事件(像是用户操作,定时器,XHR事件),这个相关的表达式必须通过$apply方法应用到作用域以便所有的监听器都正确的更新。
指令和创建作用域
在大多数情况,指令和作用域交互不创建新的作用域。无论如何,一些指令,像是ng-controller和ng-repeat,创建子作用域并且将子作用域赋予相对应的dom元素上。你可以从dom元素上使用angular.element(aDomElement).scope()函数获取作用域。查看指令文档了解更多的关于作用域隔离的信息。
作用域和控制器:
作用域和控制器在下面的情况下相互作用:
- 控制器使用作用域暴露方法给模板
- 控制器定义方法可以改变模型
- 控制器可以注册监视器到模型,在控制器的行为执行后立即执行。
查看ng-controller了解更多信息
作用域$watch性能考虑
作用域脏检查属性变动在angular中是一个常规的操作,所以脏检查函数需要尽可能的快。应小心脏检查函数中没有任何的dom访问,dom访问的速度要比访问javascript对象慢很多。
作用域$watch深度
脏检查可以基于三种策略完成:引用、集合内容、和值。三种策略侦测变化的类型不同,并且他们的性能也很大的不同。
- 监测基于引用(scope.$watch(watchExpression,listener))当监视的表达式整体返回值转变成另一个新值时会检测到变化。如果这个值是一个数组或对象,它们内部的变化则无法监测到。这是非常高效的一种策略。
- 监测基于集合内容(scope.$watchCollection(watchExpression,listener))检测一个数组或一个对象内的变化:当项目被添加,删除,或者重新排序时会被监测到。这种监测是浅监测 – 它不能到达内部集合。监测集合的内容比监测引用资源开销更大,因为集合的内容拷贝需要维护。然而,这种策略尝试用最小copy需求。
- 根据值来侦测 (scope.$watch
(watchExpression, listener,
true)
) 任意的内部数据结构中到变化,这是最权威的变化机制,但是资源消耗更大一些,并且全部拷贝对于内部数据结构是要每一个都更新一边。
与浏览器事件循环的集成:
例子描述angular交互基于浏览器的事件循环。
- 浏览器的事件循环等待一个事件完成。事件希望是交互的 ,时间时间,网络事件。
- 时间回调函数被执行后。这个维护javascript的技术等级。
- 第一次执行callback时,浏览器离开了设置了javascript的文件到相对应的读者判断了它的喜好程度,
Angular 修改普通的JavaScript流提供它自己的事件处理循环。这样分割了javascript为典型和angular执行上下文。只有操作应用在Angular执行上下文中才会受益于Angular数据绑定,一行处理,属性监测,等。你也可以使用apply()在javascript中进入到Angular执行上下文,请记住在大多数地方(controllers,services) apply 已经被指令调用用来处理时间。一个显式的调用只有在实现自定义事件的会调用使用,或在工作在第三方的库的回调中。
- 进入Angular执行上下文通过调用scope.$apply(stimulusFn),stimulusFn是你希望在Angular上下文中执行的函数。
- Angular执行sitimulusFn(),通过修改应用的状态。
- Angular进入编译循环。这个循环由两个小循环构成,一个用来处理evalAsync队列,另一个用来处理监听列表。这个编译循环将一直迭代直到这个模型稳定,这意味着evalAsync队列为空并且
- $evalAsync队列用于调度工作,这需要发生在当前的堆栈帧外,在浏览器渲染视图之前。这通常使用setTimeout(0)实现,但是setTimeout(0)方式慢,并且因为浏览器渲染页面时在事件执行之后,所以可能视图还会闪烁。
- watch列表是一个自从最后一次便利后的表达式里的值的修改集合。如果有一个修改被检测到了,那么watch函数被调用用于更新dom为新的值。
- 一旦angular $digest循环完成,执行就会脱离angular 和 js上下文。这之后是浏览器重新渲染dom去呈现出变化。
Here is the explanation of how the Hello world
example achieves the data-binding effect when the user enters text into the text field.
这里解释一下Hello world的演示程序,当用户在文本域中输入文字的时候就呈现出了数据绑定的效果。
- 在编译阶段:
- ng-model和input指令设置一个keydown监听器在input control.
- interpolation设置一个$watch用于通知name的修改。
- 在运行时阶段:
- 在input control上按下X键来让浏览器发出keydown事件。
- 这个input指令采集指令去修改input的value并且调用$apply去更新angular执行下下文中的应用模型。
- Angular 应用 name=”x”到model.
- 这个递归循环开始
- 这个$watch列表检测到name属性上有修改,并且通知interpolation,从而修改dom。
- angular离开这个执行上下文,并且结束keydown时间在js框架中的使用。
- 浏览器重新渲染这个视图基于更新的文本。