之前我们探讨过Angular的执行流程,在一切准备工作就绪后(我是指所有directive和service都装载完毕),接下来其实就是编译dom(从指定的根节点开始遍历dom树),通过dom节点的元素名(E),属性名(A),class值(C)甚至注释(M)匹配指令,进而完成指令的compile,PReLink,postLink,这期间就有可能伴随着作用域的创建和继承(有些指令通过scope字段要求创建自己的(孤立)作用域),从而形成一个作用域(scope)的继承关系。
下面的代码:
compile(element)(scope);
开始编译dom树,传递的element
是应用的根节点(有ng-app属性的节点或者手动bootstrap(element,...)
的节点),而传递的scope
则是唯一的根作用域(实质上是$RootScopeProvider
服务返回的一个单例),与根节点对应。scope.$apply(..)
进行digest
进行脏检查
,开始一些初始化工作。injector.invoke(['$rootScope', '$rootElement', '$compile', '$injector', function bootstrapApply(scope, element, compile, injector) { scope.$apply(function() { element.data('$injector', injector); compile(element)(scope); }); }]);
接下来我们讲的内容都是围绕
rootScope.js
,对于Scope的实现和一些概念,大家可以先参考这篇文章构建自己的AngularJS,第一部分:Scope和Digest,建议看原文。
前面提到了根作用域$rootScope
,其实就是Scope类的一个实例,我们通过简单的依赖注入的方式就可以获取到它,像这样:
var injector = angular.injector(['ng']);injector.invoke(['$rootScope', function (scope) { console.log(scope);}]);
从控制台中可以很清晰地看到$rootScope
对象的全部属性和方法,所以我们直接看下Scope类的定义来进行下对照:
function Scope() { // 省略属性定义}Scope.prototype = { constructor: Scope, $new: function(isolate) {...}, $watch: function(watchExp, listener, objectEquality) {...}, $watchGroup: function(watchExpressions, listener) {...}, $watchCollection: function(obj, listener) {...}, $digest: function() {...}, $destroy: function() {...}, $eval: function(expr, locals) {...}, $evalAsync: function(expr) {...}, $apply: function(expr) {...}, $applyAsync: function(expr) {...}, $on: function(name, listener) {...}, $emit: function(name, args) {...}, $broadcast: function(name, args) {...}};
$watch
,$apply
和$digest
方法,以及处理自定义事件(消息传递)的$on
, $emit
和$broadcaset
方法,这些我们稍后会讲到。Scope
new出来的实例就是一个简单的object,没有任何的getter和setter,我们可以很方便的直接向里面添加修改任何自定义属性,像这样:scope.hello='world';
。为什么会说成作用域树?我们其实知道作用域之间是通过原型链继承的,又或者是没有任何继承关系的孤立作用域单独存在的。
带着这样的疑问,首先我们假设有以下的dom结构:
<A> <B> <F></F> </B> <C> <D></D> </C> <E></E></A>
对照这样的dom结构和假设,我们可以画出这样的一张图(原图):
从这张图里面我们可以看出的不仅是作用域的继承关系还有作用域之间及父子兄弟关系:
所有的作用域之间(也包括孤立作用域)根据自身所处的位置都存在以下这些关系:
$root
来访问跟作用域$parent
来访问父作用域$childHead
($childTail
)访问头(尾)子作用域prevSibling
($nextSibling
)访问前(后)一个兄弟作用域$emit
(向上冒泡),broadcast
(向下广播)所有的作用域都引用同一个$$asyncQueue
和$$postDigestQueue
上面这张图能够画出来都归功于自
$new
这个方法的实现。
代码其实很简单,就是返回一个(child)Scope的实例:
$new: function(isolate) { var child; // isolate参数用来作为是否创建孤立作用域的标志 if (isolate) { child = new Scope(); child.$root = this.$root; // 保持$$asyncQueue和$$postDigestQueue的唯一性 child.$$asyncQueue = this.$$asyncQueue; child.$$postDigestQueue = this.$$postDigestQueue; } else { // 实现原型继承 // $ChildScope构造器只在第一次调用$new方法时才会被创建 if (!this.$$ChildScope) { this.$$ChildScope = function ChildScope() { this.$$watchers = this.$$nextSibling = this.$$childHead = this.$$childTail = null; this.$$listeners = {}; this.$$listenerCount = {}; this.$id = nextUid(); this.$$ChildScope = null; }; this.$$ChildScope.prototype = this; } child = new this.$$ChildScope(); } // 维护作用域之间的父子兄弟关系 child['this'] = child; child.$parent = this; child.$$prevSibling = this.$$childTail; if (this.$$childHead) { this.$$childTail.$$nextSibling = child; this.$$childTail = child; } else { this.$$childHead = this.$$childTail = child; } return child;}
我们在controller或者directive的link方法中经常会使用$watch
方法,来监听当作用域的某个值发生变化时,采取什么样的操作。
我们可以在控制台里写一个例子(利用$rootScope),像这样:
var injector = angular.injector(['ng']);injector.invoke(['$rootScope', function (scope) { // 获取scope对象到全局 window.rootScope = scope;}]);rootScope.a = 'hello';// 监听scope.a的值rootScope.$watch('a', function (newVal, oldVal) { console.log(arguments)});// 程序初始化时digestrootScope.$digest();//修改scope.a的值,并进行digest脏检查rootScope.$apply(function (scope) { scope.a = 'world';});
看到控制台下面的日志信息如下:
["hello", "hello", Scope] // 初始化digest,触发回调,newVal和oldVal一样["world", "hello", Scope] // 修改scope.a的值后,触发回调,newVal和oldVal不一样
所以我们经常会有这样的代码来区别第一次初始化和值改变:
rootScope.$watch('a', function (newVal, oldVal) { if (newVal !== oldVal) { console.log('change'); }});
从上面的代码,便可以看出我们使用$watch
方法注册监听函数来响应当作用域中某个变量发生变化时的操作,利用$apply
或者$digest
方法来触发监听函数的执行。
所以$watch
函数所做的工作其实就是作用域中变量和关联的监听函数的存储,
看看代码:
$watch: function(watchExp, listener, objectEquality) { // 参数objectEquality进行严格比较,像object,array这种进行非引用比较而是递归值比较 // 利用$parse服务转换成函数,用于获取作用域里的变量值 var get = $parse(watchExp); if (get.$$watchDelegate) { return get.$$watchDelegate(this, listener, objectEquality, get); } // watcher对象是存储的元单位 // watch.fn 存储监听函数 // watch.last 记录变量改变之前的值 // watch.eq 是否进行严格匹配 var scope = this, array = scope.$$watchers, watcher = { fn: listener, last: initWatchVal, get: get, exp: watchExp, eq: !!objectEquality }; lastDirtyWatch = null; if (!isFunction(listener)) { watcher.fn = noop; } // 第一次初始化$$watchers为数组 if (!array) { array = scope.$$watchers = []; } // 存储数据 array.unshift(watcher); // 返回函数,可用于取解除该监听 return function deregisterWatch() { arrayRemove(array, watcher); lastDirtyWatch = null; };}
之前我们用$watch
方法,存储了监听函数,当作用域里的变量发生变化时,调用$digest
方法便会执行该作用域以及它的所有子作用域上的相关的监听函数,从而做一些操作(如:改变view)
不过一般情况下,我们不需要手动调用$digest
或者$apply
(如果一定需要手动调用的话,我们通常使用$apply
,因为它里面除了调用$digest
还做了异常处理),因为内置的directive和controller内部(即Angular Context之内)都已经做了$apply
操作,只有在Angular Context之外的情况需要手动触发$digest
,如: 使用setTimout修改scope(这种情况我们除了手动调用$digest
,更推荐使用$timeout服务
,因为它内部会帮我们调用$apply
)。
举个controller的例子:
angular.module('myApp',[]) .controller('MessageController', function($scope) { setTimeout(function() { $scope.message = 'Fetched after 2 seconds'; //$scope.$apply(function() { // $scope.message = 'Fetched after 2 seconds'; //}); }, 2000); });
正确的方式是注释掉的那一段(用$apply
包裹),否则视图(如:{{message}}
)将不会得到更新。
看一下源代码(这里精简成最核心的代码片段):
$digest: function() { // ...省略若干代码 // 外层循环至少执行一次 // 如果scope中被监听的变量一直有改变(dirty为true),那么外层循环会一直下去(TTL减1),这是为了防止监听函数有可能改变scope的情况, // 另外考虑到性能问题,如果TTL从默认值10减为0时,则会抛出异常 do { dirty = false; current = target; /// 执行异步操作evalAsync while (asyncQueue.length) { try { asyncTask = asyncQueue.shift(); asyncTask.scope.$eval(asyncTask.expression); } catch (e) {
新闻热点
疑难解答