写过Angular应用的同学们或多或少都会注意到Angular框架在幕后会根据应用结构创建很多个scope,这些scope也许是继承自它的父节点的scope,也可能是隔离scope(Isolated Scope)。但是它们最终的父节点都是$rootScope
。$rootScope
是全局唯一的一个scope,它由Angular在应用启动之初就被创建。
现在我们就来探究一下scope的树形继承结构。具体分为一下几个话题:
1. scope继承的根基 - JavaScript原型继承
2. scope的生命周期
3. scope继承和digest循环
本节主要聚焦于第一个和第二个话题。
scope继承的根基 - JavaScript原型继承
不要以为Angular为了实现scope的继承玩出了什么新花样,在底层实现上,它还是依赖于JavaScript本身采用的原型继承(Prototypal Inheritance)。因此在学习Angular中的scope继承机制前,花时间了解一下JavaScript原型继承是十分必要的。
这里并不打算花太多的篇幅来阐述JavaScript原型继承是什么,有兴趣的同学可以移步这里学习一下相关概念。
当然本文还是会结合源代码来说说原型继承到底是怎么一回事。
首先,来看看Scope这个对象的基本结构:
function Scope() {
this.$id = nextUid();
// 更多的属性定义在这里
}
Scope.prototype = {
constructor: Scope,$new: function(isolate,parent) { /*方法定义*/ },$watch: function(watchExp,listener,objectEquality,prettyPrintExpression) {/*方法定义*/}
// 更多的scope方法
}
可以看到,除了scope类型的属性都定义在了Scope function中,它的方法全部都定义了Scope.prototype
这个对象上。而这个prototype也就是所谓的”原型”。而任何Scope类型的对象,都能够间接地访问Scope类型的原型中的方法。比如在调用scope.$new()
时,会首先尝试访问scope对象本身,发现它并没有定义$new()
,于是转而求助scope.prototype
对象,发现该对象上定义了$new()
,于是实际上调用的就是scope.prototype.$new()
。
这只是一层原型继承,而所谓的原型继承链则是好多个prototype的链式关联。比如scope.prototype
对象本身也会存在一个prototype属性,那么如果在scope.prototype
对象上仍然找不到需要访问的属性,那么会继续在scope.prototype
对象的prototype对象上继续寻找,一直到这个链式结构的尽头。
对原型继承有了最基本的了解后,我们来看看scope的生命周期。谈到生命周期,就没法离开创建和销毁。对于scope而言,创建和销毁分别对应着$new
以及$destroy
方法。
首先来看看创建:
$new: function(isolate,parent) {
var child;
parent = parent || this;
if (isolate) {
child = new Scope();
child.$root = this.$root;
} else {
if (!this.$$ChildScope) {
this.$$ChildScope = createChildScopeClass(this);
}
child = new this.$$ChildScope();
}
child.$parent = parent;
child.$$prevSibling = parent.$$childTail;
if (parent.$$childHead) {
parent.$$childTail.$$nextSibling = child;
parent.$$childTail = child;
} else {
parent.$$childHead = parent.$$childTail = child;
}
if (isolate || parent != this) child.$on('$destroy',destroyChildScope);
return child;
}
首先,在创建scope的过程中可以接受两个参数:
1. isolated: 它是一个布尔值。用于指定创建的scope是否为一个隔离scope。
2. parent:它传入另外一个scope对象。传入的scope对象会被指定为当前正在创建的scope的父亲。
那么,首先我们来看看当这两个参数什么都不传会发生些什么。
如果不传入parent,那么当前被调用的scope对象会被作为新创建的scope的父亲:parent = parent || this
。
然后会判断当前scope上是否存在$$ChildScope
这个属性,如果不存在则创建一个:
function createChildScopeClass(parent) {
function ChildScope() {
this.$$watchers = this.$$nextSibling =
this.$$childHead = this.$$childTail = null;
this.$$listeners = {};
this.$$listenerCount = {};
this.$$watchersCount = 0;
this.$id = nextUid();
this.$$ChildScope = null;
}
ChildScope.prototype = parent;
return ChildScope;
}
可见,在这里设置了子scope的原型继承关系:ChildScope.prototype = parent
。
而这里我们正好也顺便看看一个每个子scope会拥有哪些属于自己的属性:
1. $$watchers
以及$$watchersCount
:用来保存scope上注册的watchers以及对应的计数信息。
2. $$listeners
以及$$listenerCount
:用来保存scope上注册的监听器以及对应的计数信息。
3. $$nextSibling
,$$childHead
,$$childTail
:下一个兄弟节点,第一个孩子节点和最后一个孩子节点的引用信息。这部分和scope继承结构的遍历有关。
4. $id
:单调递增的ID,用于调试。
因此,创建的子scope实际上就是上述ChildScope
类型的一个实例。紧接着就是将新创建的子scope(即下面代码的child对象)和整个继承树中的其它部分建立联系:
child.$parent = parent;
// 孩子的前一个兄弟节点为父亲的最后一个孩子
child.$$prevSibling = parent.$$childTail;
if (parent.$$childHead) { // 当父节点还存在另外的孩子节点时
parent.$$childTail.$$nextSibling = child;
parent.$$childTail = child;
} else { // 当父节点没有另外的孩子节点时
parent.$$childHead = parent.$$childTail = child;
}
最后,当子scope为隔离scope或者子scope的父亲不是当前scope时,需要显式地声明一个回调函数用于销毁事件:
if (isolate || parent != this) child.$on('$destroy',destroyChildScope);
这是因为在上述两种情况下,原型继承并没有发生作用。原因是压根就没有对原型继承链进行设置,即没有调用:ChildScope.prototype = parent
。
而关于scope的$on
方法,属于scope的事件机制的一部分,事件机制将会在后续的文章中单独介绍。
从上面的代码来看,scope的创建过程并不复杂。主要是设置好原型继承链并将新创建的scope和已经存在的scope树形继承结构进行关联。
那么scope的销毁过程又是如何进行的呢?废话不说,直接上源代码:
$destroy: function() {
// 避免重复销毁
if (this.$$destroyed) return;
var parent = this.$parent;
this.$broadcast('$destroy');
this.$$destroyed = true;
if (this === $rootScope) {
// 当销毁的对象为根scope时,销毁整个应用
$browser.$$applicationDestroyed();
}
incrementWatchersCount(this,-this.$$watchersCount);
for (var eventName in this.$$listenerCount) {
decrementListenerCount(this,this.$$listenerCount[eventName],eventName);
}
// 对现有的树形继承结构进行调整,从树中删除当前正在被销毁的节点,等待垃圾回收
if (parent && parent.$$childHead == this) parent.$$childHead = this.$$nextSibling;
if (parent && parent.$$childTail == this) parent.$$childTail = this.$$prevSibling;
if (this.$$prevSibling) this.$$prevSibling.$$nextSibling = this.$$nextSibling;
if (this.$$nextSibling) this.$$nextSibling.$$prevSibling = this.$$prevSibling;
// 无效化scope上的所有方法以及destroy方法本身
this.$destroy = this.$digest = this.$apply = this.$evalAsync = this.$applyAsync = noop;
this.$on = this.$watch = this.$watchGroup = function() { return noop; };
this.$$listeners = {};
this.$$nextSibling = null;
cleanUpScope(this);
}
上述代码主要做了几件事:
1. 向当前scope的所有子scope广播销毁事件。
2. 被销毁scope为根scope时的特殊处理。
3. 调整watchers和listeners的计数信息。
4. 调整继承树结构。
5. 将被销毁scope的方法无效化,防止误操作。
6. 将被销毁scope的关联关系抹去,防止误操作。
关于第一点,在后续专门讨论事件机制的文章中再进行讨论。
而对销毁根scope的特殊处理,实际上是去掉关联到window
对象上的hashchange
和popstate
事件的callback:
self.$$applicationDestroyed = function() {
jqLite(window).off('hashchange popstate',cacheStateAndFireUrlChange);
};
至于调整watchers以及listeners的计数信息,其实也很直观:
function incrementWatchersCount(current,count) {
do {
current.$$watchersCount += count;
} while ((current = current.$parent));
}
function decrementListenerCount(current,count,name) {
do {
current.$$listenerCount[name] -= count;
if (current.$$listenerCount[name] === 0) {
delete current.$$listenerCount[name];
}
} while ((current = current.$parent));
}
需要注意的一点是:除了需要对当前正被销毁的scope的计数信息进行维护为,还需要维护它所有的父亲scope的计数信息。这一点从上面两个函数的while循环中可见一斑。
调整继承树形结构,更像是对链表这种数据结构的基本功练习。不明白的话,画个图仔细体会一下应该没有什么难的。
最后的两点,对被销毁scope上各种方法设置为noop,同时也将被销毁scope的各种关联关系抹去,目的都是防止误操作。
至此,scope生命周期中最重要的创建和销毁就介绍完毕了。在阅读源代码前,我觉得Angular的处理肯定用了什么黑魔法,但是在阅读后,觉得还是基本功更重要。再复杂的程序,背后的原理也许还是那么几个最根本的东西。