作用域(Scope)
- 是一个存储应用数据模型的对象
- 为 表达式 提供了一个执行上下文
- 作用域的层级结构对应于 DOM 树结构
- 作用域可以监听 表达式 的变化并传播事件
作用域特点
作用域提供了
$watch
方法监听数据模型的变化作用域提供了
$apply
方法把不是由Angular触发的数据模型的改变引入Angular的控制范围内(如控制器,服务,及Angular事件处理器等)作用域提供了基于原型链继承其父作用域属性的机制,就算是嵌套于独立的应用组件中的作用域也可以访问共享的数据模型(这个涉及到指令间嵌套时作用域的几种模式)
作用域提供了 表达式 的执行环境,比如像
{{username}}
这个表达式,必须得是在一个拥有这个属性的作用域中执行才会有意义,也就是说,作用域中可能会像这样scope.username
或是$scope.username
,至于有没有$
符号,看你是在哪里访问作用域了
作为数据模型的作用域
作用域是控制器和视图之间的“胶水”。在模板链接阶段,指令设置好作用域的$watch
表达式。$watch
使得指令能知晓属性的改变,这使得指令能重新渲染和更新DOM中的值。
控制器和指令都持有作用域的引用,但是不持有对方的。这使得控制器能从指令和DOM中脱离出来。这很重要,因为这使得控制器完全不需要知道view的存在,这大大改善了应用的测试。
可以很简单地理解为有以下两个链条关系:
- 控制器 –> 作用域 –> 视图(DOM)
- 指令 –> 作用域 –> 视图(DOM)
作用域包含了渲染视图时所需的功能和数据,它是所有视图的唯一源头。可以将作用域理解成视图模型(view model)。
作用域分层结构
每个AngularJS应用有且仅有一个根作用域 $rootScope
,它可作用于整个应用中,可以在各个 controller 中使用,是各个 controller 中 scope 的桥梁。
一个应用可以有多个作用域,因为有一些指令会生成新的子作用域。当新作用域被创建的时候,他们会被当成子作用域添加到父作用域下,这使得作用域会变成一个和相应DOM结构一样的树状结构。
当AngularJS执行表达式{{username}}
,它会首先查找和当前节点相关的作用域中叫做username的属性。如果没找到,那就会继续向上层作用域搜索,直到根作用域。在Javascript中,这被称为原型类型的继承,子作用域以原型的形式继承自父作用域。
基于作用域的事件传播
作用域可以像DOM节点一样,进行事件的传播。主要是有两个方法:
broadcasted
:从父级作用域广播至子级 scopeemitted
:从子级作用域往上发射到父级作用域
$on,$emit,$broadcast
使用代码
<div ng-app="Demo">
<div ng-controller="testCtrl as ctrl">
{{ctrl.number||'Here where receive a number from childScope'}}
<div ng-controller="childCtrlOne as childOne">
<input type="button" ng-click="childOne.toFatherScope()" value="click me" />
</div>
<div ng-controller="childCtrlTwo as childTwo">
{{childTwo.number||'Here where receive a number from fatherScope'}}
</div>
</div>
</div>
(function () {
angular.module("Demo", [])
.controller("testCtrl",["$scope",testCtrl])
.controller("childCtrlOne",["$scope",childCtrlOne])
.controller("childCtrlTwo",["$scope",childCtrlTwo])
function testCtrl($scope){
var vm = this;
$scope.$on("toFather",function(e,v){
vm.number = v;
$scope.$broadcast("toChild",v);
})
};
function childCtrlOne($scope){
var count = 0;
this.toFatherScope = function(){
count += 1;
$scope.$emit("toFather",count);
}
}
function childCtrlTwo($scope){
var vm = this;
$scope.$on("toChild",function(e,v){
vm.number = v;
})
}
}());
作用域的生命周期
$scope对象的生命周期处理有四个不同阶段。
(1)创建
root scope
是在应用程序启动时由 $injector
创建的。在创建控制器或指令时,AngularJS会用$injector
创建一个新的作用域,并在这个新建的控制器或指令运行时将作用域传递进去。
在模板链接阶段,有些指令会创建新的子作用域。
(2)链接
当Angular开始运行时,所有的$scope
对象都会附加或者链接到视图中。所有创建$scope
对象的函数也会将自身附加到视图中。这些作用域将会注册当Angular应用上下文中发生变化时需要运行的 $watch
函数,Angular通过这些函数获知何时启动事件循环。
(3)更新
为了正确地观测到模型变化,你需要并且只能在scope.$apply()
中改变他们。(AngularJS的API会隐式地这么做,所以在控制器或者在$http
,$timeout
等服务中你不需要额外的调用$apply
)。
在$apply
的最后,AngularJS会在根作用域中执行一个$digest
循环,它会将变化传递给所有子作用域。在$digest
循环中,所有的$watch
表达式或者函数都会被检测,来观察模型的变化。如果有变化被检测到了,$watch
的监听回调就会被调用。
(4)销毁
当一个$scope
在视图中不再需要时,这个子作用域的创建者就会负责用scope.$destroy()
API来将它销毁。这会停止$digest
再调用子作用域,并且让作用域占用的内容能够被回收。
作用域和指令
指令类型
在编译阶段,编译器在DOM中匹配指令。指令通常分为两种:
观察型的指令,例如双花括号表达式
{{expression}}
,会用$watch
来注册一个监听者。无论表达式什么时候改变,这类型的指令都会被通知,并且能更新视图。监听者型的指令,比如ng-click,会向DOM注册一个监听者。当DOM监听者触发,指令会执行相关的表达式并且使用
$apply
方法更新视图。
综上,当一个外界事件(比如用户操作,计时器或者XHR)触发时,相应的表达式必须在$apply()
方法内来由其相应的作用域调用,这样所有的监听者才会被正确地更新。
会创建作用域的指令
大部分情况下,指令和作用域交互但不会产生新的作用域实例。但是,有些指令,比如ng-controller
和ng-repeat
会创建新的作用域,并关联到相应的DOM元素上,你可以使用angular.element(aDomElement).scope()
方法来获得某一个DOM元素相关的作用域。
作用域和控制器
作用域和控制器在以下几种情况下交互:
- 控制器通过作用域来向模板暴露方法
- 控制器里定义能改变模型(作用域的属性)的方法(行为)
- 控制器在模型上注册了观察者。这些观察者会在控制器行为执行后立即被执行
$rootScope.Scope对象详解
$rootScope.Scope
:可以使用$injector
通过$rootScope
关键字检索的一个根作用域。
可以通过$new()
方法创建子作用域。(大多子作用域是在HTML模板被执行编译时自动生成)
格式:
$rootScope.Scope([Providers],[instanceCache])
参数:
- Providers:当前作用域需要被提供的服务工厂Map。默认是ng。
- instanceCache:为需要providers追加/重写的服务提供预实例化服务。
方法:
$new(isolate);
创建一个新的子作用域。
父作用域将会广播$digest()
和$digest()
事件。作用域可以使用$destroy()
从作用域的层级结构中移除。
$destroy()
使其所需的范围和它的子作用域范围内永久地从父作用域分离,从而停止参与模型变化检测和侦听通知调用。- isolate:boolean类型。如果值是true,那么这个scope不会从父scope继承原型。作用域是独立的,在这里不能看见父scope的属性。
当写小窗户组件的时候,这将是很实用的去防止不小心读取到其父级的状态。 $watch(watchExpression,[listener],[objectEquality]);
注册一个监听器的回调函数,该函数在watchExpression变化的时候被执行。
watchExpressions表达式每次执行都会产生一次$digest()
,并且返回一个将会被监听的值。($digest()
发现watchExpressions发生变化而执行多次,并且每次都是幂等的)
监听只有在当前的watchExpressions与之前的值不相等时被调用。变动是根据angular.equals
函数判断的。需要保存对象比较后的值,也需要用到angular.copy
。这也意味着看复杂的选项将不利于记忆和性能影响。
监听可能会改变模型,这可能会引发其他监听的变化。Ng会一直执行直到监听的值稳点。重播迭代极限是10,为了防止陷入无限循环的死锁。- watchExpressions:string或者function类型。每个
$digest
循环周期的表达式,返回值的变化会触发调用监听。 - listener: watchexpression的返回值改变时发生回调。
- objectEquality:使用
angular.equals
代替引用对象的相等性比较(对象的深度监听)。 $watchGroup(watchExpressions,listener);
针对watchexpressions数组变量的$watch()
。集合里的任何一个表达式变化都将引发监听的执行。
watchExpressions数组里的每一项都被标准的$watch()
操作观察,并且审查每一次的$digest()
去观察每一项是否变化。
当watchExpressions数组里的任何一项发生变化即执行。$watchCollection(obj,listener);
浅度的观察对象属性,并且在其变化时执行(对于数组,这意味着看数组项;对于对象,这意味着看属性)。如果检测到更改,则该侦听器将被触发。$digest();
处理所有的当前作用域和它的子作用域的监听。因为监听可能改变模型,所以$digest()
会一直执行直到模型稳定。这意味这他可能进入无限循环。如果迭代次数超过10,这个函数将抛出“Maximum iteration limit exceeded”错误。$destroy();
从父域中删除当前的scope(及其所有的子scope)。删除意味着$digest()
不再传播到目前作用域及其子作用域。删除也意味着目前的作用域符合垃圾集合的条件。$eval([expression],[locals]);
在当前作用域上执行表达式并返回结果。表达式的任何异常将传播(捕获)。在求Angular表达式的值的时候有用。$evalAsync([expression]);
在稍后的时间点上执行当前范围的表达式(异步)。$apply([exp]);
$apply()
用来在Angular框架外执行angular内部的表达式。(例如浏览器的DOM事件,setTimeout,XHR或第三方库)。$on(name,listener);
监听一个给定类型的事件。- name:监听的事件名。
- listener:当事件发生时调用的函数。
$emit(name,args);
向上级已注册的作用域传播指定的事件,直到根作用域。- name:发出的事件名称。
- args:一个或多个可选参数,将传递到事件侦听器。
$broadcast(name,args);
向下级已注册的作用域广播指定的事件。- name:发出的事件名称。
- args:一个或多个可选参数,将传递到事件侦听器。
$new,$destroy,$watch
使用代码
<div ng-app="Demo" ng-controller="testCtrl as ctrl">
<input ng-model="ctrl.text.words" />
<input ng-list="," ng-model="ctrl.list" />
</div>
(function () {
angular.module("Demo", [])
.run(["$rootScope",rootScope])
.controller("testCtrl",["$scope",testCtrl])
function rootScope($rootScope){
var rootScope = $rootScope;
var _scope = rootScope.$new();
_scope.value = "Hello World";
_scope.$destroy();//$$destroyed:true
};
function testCtrl($scope){
this.text = { words:"Hello World",id:1};
$scope.$watch("ctrl.text",function(n,o){
console.log(n,o); // n 新值 o 旧值
},true);
this.list = ["a","b","c","d"];
$scope.$watchCollection("ctrl.list",function(n,o){
console.log(n,o); // n 新值 o 旧值
});
};
}());
$watch方法,$digest方法,$apply方法三者之间的关系
参考自理解 watch, w a t c h , apply 和 $digest — 理解数据绑定过程
浏览器接收一个事件的标准的工作流程应该是:
接收事件 --> 触发回调 --> 回调执行结束返回 --> 浏览器重绘DOM --> 浏览器返回等待下一个事件
当浏览器调用AngularJS上下文之外的Javascript代码时,AngularJS是不知道模型的更改的。要正确处理模型的更改,就要使用$apply
方法进入AngularJS的执行上下文。只有在$apply
方法内执行的模型修改才会正确地被AngularJS处理。比如,一个指令监听DOM事件,比如ng-click
,它必须在$apply
方法中来执行表达式。
执行完表达式之后,$apply
会进入$digest
阶段。在$digest
阶段作用域会检查所有的$watch
表达式,并将它们和之前的值比较。这个脏值检查工作是异步执行的。这意味着赋值语句,如$scope.username="angular"
不会马上导致$watch
被通知,取而代之的是它会等到$digest
阶段才被通知。这种方式是合理的,因为它将多个模型的更新整合到一个$watch
通知里,并且保证了一个$watch
通知期间不会有其他$watch
执行。如果一个$watch
改变了模型的值,那么它会产生一个额外的$digest
阶段。
如果$digest
循环运行10次或者更多次,Angular应用会抛出一个异常,同时停止运行。
$watch
$scope
对象上的$watch
方法会给Angular事件循环内的每个$digest
调用装配一个脏值检查。如果在表达式上检测到变化,Angular总是会返回$digest
循环。
语法:
$watch(watchExpression, listener, objectEquality);
每个参数的说明如下:
- watchExpression(必选):监听的对象,它可以是一个string,将被计算为表达式 ,或函数如
function(){return $scope.name}
。 - listener(必选):当watchExpression(监听对象)变化时会被调用的函数或者表达式,它接收3个参数:newValue(新值), oldValue(旧值), scope(作用域的引用)
- objectEquality(可选):是否深度监听,如果设置为true,它告诉Angular检查所监控的对象中每一个属性的变化. 如果你希望监控数组的个别元素或者对象的属性而不是一个普通的值, 那么你应该使用它。
监听函数会在初始化时被调用一次,而此时newVal和oldVal的值都为undefined
(并且是相等的)。在这种情况下,如果正处在初始化阶段或者先前的值发生了变化,通常最好是检查内部的表达式。在监控函数内很容易实现这一检查,就像这样:
<body ng-controller="MainCtrl">
<input ng-model="name" />
Name updated: {{updated}} times.
</body>
app.controller('MainCtrl', function($scope) {
$scope.name = "Angular";
$scope.updated = 0;
$scope.$watch('name', function(newValue, oldValue) {
if (newValue === oldValue) { return; } // AKA first run
$scope.updated++;
});
});
注意:永远不要在控制器中使用$watch
,因为它会使控制器难以测试。最好是放在相应的服务中。
作用域的$watch
操作要注意的
因为在Angular中对作用域进行脏值检查($watch
)实时跟踪数据模型的变化是一个非常频繁的操作,所以,进行脏值检查的这个函数必须是高效的。一定要注意的是,用 $watch
进行脏值检查时,一定不要做任何的DOM操作,因为DOM操作拖慢甚至是拖垮整体性能的能力比在 JavaScript对象上做属性操作高好几个数量级。
$watchCollection
此外,Angular还允许我们为对象的属性或者数组的元素设置浅监控,然后只要属性发生变化就触发监听器回调。
使用$watchCollection
可以检测对象或数组何时发生了变化,以便确定对象或数组中的条目是何时添加、移除或者移的。$watchConllection
的行为与$digest
循环中标准的$watch
的行为一样,我们甚至可以把它当作标准$watch
。
$watchCollectiion()
函数接受2个参数:
- obj(字符串/函数)
这个对象就是一个要监控的对象。如果传入一个字符串,它将被当作Angular表达式求值。
如果传入的是一个函数,将在当前作用域中被调用,并且会返回要监控的值。
- listener(函数)
这个回调函数会在集合发生变化时触发。类似于$watch
函数,这个函数会被来自$watch
的新集合触发调用,而原来的集合(先前集合的副本)以及所在的作用域也随之生效。
$watchConllection()
函数和$watch
函数一样也返回一个注销函数。调用这个注销函数时,也会取消集合上的$watch
。
$scope.$watchCollection('names',
function(newNames, oldNames, scope) {
// names集合已经发生了变化
});
$digest循环
Angular会运行一个函数$digest
来检查scope模型中的数据是否发生了变化。
在$digest
循环中,watchers会被触发。当一个watcher被触发时,AngularJS会检测它所监听的scope模型,如果监听对象发生了变化,那么关联到该watcher的回调函数就会被调用。 这种方法叫做脏检查。
在angular程序初始化时,会将绑定对象的属性添加为监听对象(watcher),也就是说一个$scope
对象绑定了N个属性,就会添加N个watcher。
angular什么时候去脏检查呢?
angular所定义的方法中都会触发$digest
事件,比如:controller初始化的时候,所有以ng-开头的指令执行后,都会触发脏检查;用户与视图发生交互行为以后会触发脏检查。
调用$digest
方法:$scope.$digest();
$evalAsync 列表
$evalAsync()
方法是一种在当前作用域上调度表达式在未来某个时刻运行的方式。$digest
循环运行的第二个操作是执行$$asyncQueue
。可以使用$evalAsync()
方法访问这个工作队列。
$digest
循环期间,贯穿脏值检查生命周期的每个循环之间的队列都是空的,这意味着使用$evalAsync
来调用任何函数都会发生两件事情。
- 函数会在这个方法被调用的某个时刻之后执行。
- 表达式求值之后至少会执行一次
$digest
循环。
$evalAsync()
方法接受一个唯一参数:
- expression(字符串/函数)
这个表达式便是我们想要在当前作用域上执行的东西。如果传入一个字符串,Angular将会在当前作用域上使用$eval
求值该表达式。
如果传入的是一个函数,Angular将会使用传递给这个函数的scope对象执行函数求值。
$scope.$evalAsync('attribute',
function(scope) {
scope.foo = "Executed"
});
使用$evalAsync
时要注意的一些细节:
- 如果指令直接调用
$evalAsync()
,它会在Angular操作DOM之后、浏览器渲染之前运行。 - 如果控制器调用
$evalAsync()
,它也会在Angular操作DOM之后、浏览器渲染之前运行(永远不要使用$evalAsync()
来约定事件的顺序)。
无论何时,在Angular中,只要你想要在一个行为的执行上下文外部执行另一个行为,就应该使用$evalAsync()
函数。
你还可以使用它替代setTimeout()
函数,但是它可能在浏览器重新渲染视图之后导致屏幕闪烁。
$apply
AngularJS并不直接调用$digest()
,而是调用$scope.$apply()
,$apply
方法就是将$digest
方法包装了一层,然后调用$rootScope.$digest()
。因此,一轮$digest
循环在$rootScope
开始,随后会访问到所有的children scope中的watchers。
$apply()
方法接受一个可选参数,可以是string,string将被看作表达式并计算结果,也可以是函数。 当接受函数function作为参数,会执行该function并且触发一轮$digest
循环。 不接受任何参数,触发一轮$digest
循环,检查该$scope
里的所有监听的属性。
// 使用要eval的字符串调用$apply示例
$scope.$apply('message = "Hello World"');
// 使用函数的方式并给函数传入一个作用域
$scope.apply(function(scope) {
// 然后在函数中使用传入作用域
scope.message = "Hello World";
});
// 使用函数时忽略作用域
$scope.$apply(function() {
$scope.message = "Hello World";
});
// 或者通过在操作的尾部调用$apply()以强制运行$digest循环
$scope.apply();
如果你在AngularJS上下文之外的任何地方修改了model,那么你就需要通过手动调用$apply()
来通知AngularJS。
理解双向绑定
<html ng-app>
<head>
<script src="http://code.angularjs.org/1.2.25/angular.min.js"></script>
<script src="script.js"></script>
</head>
<body>
<div ng-controller="MyController">
Your name:
<input type="text" ng-model="username">
<button ng-click='sayHello()'>greet</button>
<hr>
</div>
</body>
</html>
function MyController($scope) {
$scope.username = 'World';
$scope.sayHello = function() {
$scope.greeting = 'Hello ' + $scope.username + '!';
};
}
- 编译阶段:
ng-model
和input
指令在<input>
标签中设置了一个keydown
监听器- 在
{{greeting}}
插值(也就是表达式)这里设置了一个$watch
来监测username
的变化 - 执行阶段:
- 在
<input>
输入框中按下 ‘X
’ 键引起浏览器发出一个keydown
事件 input
指令捕捉到输入值的改变调用$apply("username = 'X';")
进入Angular的执行环境来更新应用的数据模型- Angular将
username = 'X';
作用在数据模型之上,这样scope.username
就被赋值为'X'
了 $digest
轮循开始$watch
列表中监测到username
有一个变化,然后通知{{greeting}}
插值表达式,进而更新DOM- 执行离开Angular的上下文,进而
keydown
事件结束,然后执行也就退出了 JavaScript的上下文;这样$digest
完成 - 浏览器用更新了的值重新渲染视图