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

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对象上的hashchangepopstate事件的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的处理肯定用了什么黑魔法,但是在阅读后,觉得还是基本功更重要。再复杂的程序,背后的原理也许还是那么几个最根本的东西。

©️2020 CSDN 皮肤主题: 大白 设计师: CSDN官方博客 返回首页
实付0元
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值