[AngularJS面面观] 6. scope继承 - 基于原型继承的树形体系以及scope的生命周期

前端之家收集整理的这篇文章主要介绍了[AngularJS面面观] 6. scope继承 - 基于原型继承的树形体系以及scope的生命周期前端之家小编觉得挺不错的,现在分享给大家,也给大家做个参考。

写过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这个对象的基本结构:

  1. function Scope() {
  2. this.$id = nextUid();
  3. // 更多的属性定义在这里
  4. }
  5.  
  6. Scope.prototype = {
  7. constructor: Scope,$new: function(isolate,parent) { /*方法定义*/ },$watch: function(watchExp,listener,objectEquality,prettyPrintExpression) {/*方法定义*/}
  8. // 更多的scope方法
  9. }

可以看到,除了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方法

首先来看看创建:

  1. $new: function(isolate,parent) {
  2. var child;
  3.  
  4. parent = parent || this;
  5.  
  6. if (isolate) {
  7. child = new Scope();
  8. child.$root = this.$root;
  9. } else {
  10. if (!this.$$ChildScope) {
  11. this.$$ChildScope = createChildScopeClass(this);
  12. }
  13. child = new this.$$ChildScope();
  14. }
  15. child.$parent = parent;
  16. child.$$prevSibling = parent.$$childTail;
  17. if (parent.$$childHead) {
  18. parent.$$childTail.$$nextSibling = child;
  19. parent.$$childTail = child;
  20. } else {
  21. parent.$$childHead = parent.$$childTail = child;
  22. }
  23.  
  24. if (isolate || parent != this) child.$on('$destroy',destroyChildScope);
  25.  
  26. return child;
  27. }

首先,在创建scope的过程中可以接受两个参数:
1. isolated: 它是一个布尔值。用于指定创建的scope是否为一个隔离scope。
2. parent:它传入另外一个scope对象。传入的scope对象会被指定为当前正在创建的scope的父亲。

那么,首先我们来看看当这两个参数什么都不传会发生些什么。
如果不传入parent,那么当前被调用的scope对象会被作为新创建的scope的父亲:parent = parent || this

然后会判断当前scope上是否存在$$ChildScope这个属性,如果不存在则创建一个:

  1. function createChildScopeClass(parent) {
  2. function ChildScope() {
  3. this.$$watchers = this.$$nextSibling =
  4. this.$$childHead = this.$$childTail = null;
  5. this.$$listeners = {};
  6. this.$$listenerCount = {};
  7. this.$$watchersCount = 0;
  8. this.$id = nextUid();
  9. this.$$ChildScope = null;
  10. }
  11. ChildScope.prototype = parent;
  12. return ChildScope;
  13. }

可见,在这里设置了子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对象)和整个继承树中的其它部分建立联系:

  1. child.$parent = parent;
  2. // 孩子的前一个兄弟节点为父亲的最后一个孩子
  3. child.$$prevSibling = parent.$$childTail;
  4. if (parent.$$childHead) { // 当父节点还存在另外的孩子节点时
  5. parent.$$childTail.$$nextSibling = child;
  6. parent.$$childTail = child;
  7. } else { // 当父节点没有另外的孩子节点时
  8. parent.$$childHead = parent.$$childTail = child;
  9. }

最后,当子scope为隔离scope或者子scope的父亲不是当前scope时,需要显式地声明一个回调函数用于销毁事件:

  1. if (isolate || parent != this) child.$on('$destroy',destroyChildScope);

这是因为在上述两种情况下,原型继承并没有发生作用。原因是压根就没有对原型继承链进行设置,即没有调用ChildScope.prototype = parent

而关于scope的$on方法,属于scope的事件机制的一部分,事件机制将会在后续的文章中单独介绍。

从上面的代码来看,scope的创建过程并不复杂。主要是设置好原型继承链并将新创建的scope和已经存在的scope树形继承结构进行关联。

那么scope的销毁过程又是如何进行的呢?废话不说,直接上源代码

  1. $destroy: function() {
  2. // 避免重复销毁
  3. if (this.$$destroyed) return;
  4. var parent = this.$parent;
  5.  
  6. this.$broadcast('$destroy');
  7. this.$$destroyed = true;
  8.  
  9. if (this === $rootScope) {
  10. // 当销毁的对象为根scope时,销毁整个应用
  11. $browser.$$applicationDestroyed();
  12. }
  13.  
  14. incrementWatchersCount(this,-this.$$watchersCount);
  15. for (var eventName in this.$$listenerCount) {
  16. decrementListenerCount(this,this.$$listenerCount[eventName],eventName);
  17. }
  18.  
  19. // 对现有的树形继承结构进行调整,从树中删除当前正在被销毁的节点,等待垃圾回收
  20. if (parent && parent.$$childHead == this) parent.$$childHead = this.$$nextSibling;
  21. if (parent && parent.$$childTail == this) parent.$$childTail = this.$$prevSibling;
  22. if (this.$$prevSibling) this.$$prevSibling.$$nextSibling = this.$$nextSibling;
  23. if (this.$$nextSibling) this.$$nextSibling.$$prevSibling = this.$$prevSibling;
  24.  
  25. // 无效化scope上的所有方法以及destroy方法本身
  26. this.$destroy = this.$digest = this.$apply = this.$evalAsync = this.$applyAsync = noop;
  27. this.$on = this.$watch = this.$watchGroup = function() { return noop; };
  28. this.$$listeners = {};
  29.  
  30. this.$$nextSibling = null;
  31. cleanUpScope(this);
  32. }

上述代码主要做了几件事:
1. 向当前scope的所有子scope广播销毁事件。
2. 被销毁scope为根scope时的特殊处理。
3. 调整watchers和listeners的计数信息。
4. 调整继承树结构。
5. 将被销毁scope的方法无效化,防止误操作。
6. 将被销毁scope的关联关系抹去,防止误操作。

关于第一点,在后续专门讨论事件机制的文章中再进行讨论。
而对销毁根scope的特殊处理,实际上是去掉关联到window对象上的hashchangepopstate事件的callback:

  1. self.$$applicationDestroyed = function() {
  2. jqLite(window).off('hashchange popstate',cacheStateAndFireUrlChange);
  3. };

至于调整watchers以及listeners的计数信息,其实也很直观:

  1. function incrementWatchersCount(current,count) {
  2. do {
  3. current.$$watchersCount += count;
  4. } while ((current = current.$parent));
  5. }
  6.  
  7. function decrementListenerCount(current,count,name) {
  8. do {
  9. current.$$listenerCount[name] -= count;
  10.  
  11. if (current.$$listenerCount[name] === 0) {
  12. delete current.$$listenerCount[name];
  13. }
  14. } while ((current = current.$parent));
  15. }

需要注意的一点是:除了需要对当前正被销毁的scope的计数信息进行维护为,还需要维护它所有的父亲scope的计数信息。这一点从上面两个函数的while循环中可见一斑。

调整继承树形结构,更像是对链表这种数据结构的基本功练习。不明白的话,画个图仔细体会一下应该没有什么难的。

最后的两点,对被销毁scope上各种方法设置为noop,同时也将被销毁scope的各种关联关系抹去,目的都是防止误操作。

至此,scope生命周期中最重要的创建和销毁就介绍完毕了。在阅读源代码前,我觉得Angular的处理肯定用了什么黑魔法,但是在阅读后,觉得还是基本功更重要。再复杂的程序,背后的原理也许还是那么几个最根本的东西。

猜你在找的Angularjs相关文章