从Angular源码看scope(二)

前言之前我们探讨过《Angular的执行流程》,在一切准备工作就绪后(我是指所有directive和service都装载完毕),接下来其实就是编译dom(从指定的根节点开始遍历dom树),通过dom节点的元素名(E),属性名(A),class值(C)甚至注释(M)匹配指令,进而完成指令的compile,preLink,postLink,这期间就有可能伴随着作用域的创建和继承(有些指令通过scope字段要求创建自己的(孤立)作用域),从而形成一个作用域(scope)的继承关系。

下面的代码:

  1. 调用 compile(element)(scope);

开始编译dom树,传递的 element

是应用的根节点(有ng-app属性的节点或者手动 bootstrap(element,...)

的节点),而传递的 scope

则是唯一的根作用域(实质上是 $RootScopeProvider

服务返回的一个单例),与根节点对应。1. 最后通过 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); }); }]);

Scope 接下来我们讲的内容都是围绕 rootScope.js

,对于Scope的实现和一些概念,大家可以先参考这篇文章构建自己的AngularJS,第一部分:Scope和Digest,建议看原文。

Scope类前面提到了根作用域 $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) {...}};
  1. 从原型方法中,可以看到我们熟悉的 $watch

, $apply

和 $digest

方法,以及处理自定义事件(消息传递)的 $on

, $emit

和 $broadcaset

方法,这些我们稍后会讲到。1. 而由 Scope

new出来的实例就是一个简单的object,没有任何的getter和setter,我们可以很方便的直接向里面添加修改任何自定义属性,像这样: scope.hello='world';

。##### scope作用域树 为什么会说成作用域树?我们其实知道作用域之间是通过原型链继承的,又或者是没有任何继承关系的孤立作用域单独存在的。

带着这样的疑问,首先我们假设有以下的dom结构:[br]1. 节点A为根节点[br]2. 每个节点都有指令,且指令都会创建自己的(孤立)作用域[br]3. 节点E和节点F创建的是孤立作用域

对照这样的dom结构和假设,我们可以画出这样的一张图(原图):

产品经理

从这张图里面我们可以看出的不仅是作用域的继承关系还有作用域之间及父子兄弟关系:

  1. 普通的作用域通过原型链实现了继承关系,孤立作用域没有任何继承关系。1. 所有的作用域之间(也包括孤立作用域)根据自身所处的位置都存在以下这些关系: $root

来访问跟作用域1. $parent

来访问父作用域1. $childHead

( $childTail

)访问头(尾)子作用域1. prevSibling

( $nextSibling

)访问前(后)一个兄弟作用域这样的关系便形成了一个作用域树,通过它便可以完成作用域的向上(下)的遍历,从而实现后面的消息传递, $emit

(向上冒泡), broadcast

(向下广播)

  1. 所有的作用域都引用同一个 $$asyncQueue

和 $$postDigestQueue

$new方法构建作用域上面这张图能够画出来都归功于自 $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;}
$watch方法监听作用域变化我们在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;  };}
$digest方法进行脏检查之前我们用 $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) {        $exceptionHandler(e);      }      lastDirtyWatch = null;    }    // 标签语句。用于随时跳出该循环    // 该循环遍历当前作用域以及它的子作用域,并执行监听函数    traverseScopesLoop: do {      if ((watchers = current.$$watchers)) {        length = watchers.length;        //  遍历监听函数        while (length--) {          try {            watch = watchers[length];            if (watch) {              // 进行值比较或者严格的递归比较,这里考虑到一个特殊情况NaN不等于自身的情况              if ((value = watch.get(current)) !== (last = watch.last) &&                !(watch.eq ? equals(value, last) : (typeof value === 'number' && typeof last === 'number' && isNaN(value) && isNaN(last)))) {                dirty = true;  // 标记为dirty                lastDirtyWatch = watch; // 保存最后一个dirty的watch,用于下面的判断watch === lastDirtyWatch                watch.last = watch.eq ? copy(value, null) : value; // 保存被监听变量上一次的值                watch.fn(value, ((last === initWatchVal) ? value : last), current); // 执行监听函数                if (ttl 另外:    $RootScopeProvider

中提供了 digestTtl

方法,用于修改TTL的值(默认是10),可以这样修改:

angular.module('ng').config(['$rootScopeProvider', function ($RootScopeProvider) {  $RootScopeProvider.digestTtl(20);}]);

作用域事件(消息)机制 Angular在scope上通过 $on

, $emit

和 $broadcast

方法实现了自定义事件(消息)机制,这是代码解耦,实现数据共享的神器。

区别于 Backbone.Events

,这里的事件(消息)传递和接收针对的不是单一对象,而是多个对象(作用域树)

$emit

传递消息是从当前scope对象开始,通过 scope.$parent

将消息向上 冒泡

一直传递到rootScope对象

broadcast

传递消息也是从当前scope对象开始,通过复杂的作用域之间的关系,将消息向下 广播

到所有的childScope对象(貌似是深度优先的顺序)

另外,在消息传递并执行监听函数时,会有一个 event对象

会被作为参数传递给监听函数,里面有我们关心的几个字段:

{    name: 'xxx', // 消息名    targetScope: scope,  // 触发改事件的目标(起始)作用域    currentScope: scope, // 正在执行监听函数的当前作用域    stopPropagation: fn  // 阻止冒泡(只在emit时存在)    ...}

一个简单的例子:

html

    ParentCtrl        SiblingOneCtrl          SiblingTwoCtrl            ChildCtrl      

js

app.controller('ParentCtrl', function ($scope) {  $scope.$on('ChildCtrl:emit', function () {    console.log('ParentCtrl: ', arguments);  });  $scope.$on('ParentCtrl:broadcast', function () {    console.log('ParentCtrl:', arguments);  });  // 延迟执行  $scope.$evalAsync(function () {    // 向下广播传递消息    $scope.$broadcast('ParentCtrl:broadcast', 'Broadcast!');  });});app.controller('SiblingOneCtrl', function ($scope) {  $scope.$on('ChildCtrl:emit', function () {    console.log('SiblingOneCtrl:', arguments);  });  $scope.$on('ParentCtrl:broadcast', function () {    console.log('SiblingOneCtrl:', arguments);  });});app.controller('SiblingTwoCtrl', function ($scope) {  $scope.$on('ChildCtrl:emit', function () {    console.log('SiblingTwoCtrl:', arguments);  });  $scope.$on('ParentCtrl:broadcast', function () {    console.log('SiblingTwoCtrl:', arguments);  });});app.controller('ChildCtrl', function ($scope) {  $scope.$on('ChildCtrl:emit', function () {    console.log('ChildCtrl:', arguments);  });  $scope.$on('ParentCtrl:broadcast', function () {    console.log('ChildCtrl:', arguments);  });  // 向上冒泡传递消息  $scope.$emit('ChildCtrl:emit', 'Emit!');});

控制台里我们可以看到以下日志(Object为event对象):

ChildCtrl: [Object, "Emit!"]SiblingTwoCtrl: [Object, "Emit!"]ParentCtrl:  [Object, "Emit!"]ParentCtrl: [Object, "Broadcast!"]SiblingOneCtrl: [Object, "Broadcast!"]SiblingTwoCtrl: [Object, "Broadcast!"]ChildCtrl: [Object, "Broadcast!"]

消息传递路径:

emit: ChildCtrl -> SiblingTwoCtrl -> ParentCtrlbroadcast: ParentCtrl -> SiblingOneCtrl -> SiblingTwoCtrl -> ChildCtrl
$on方法注册自定义事件(消息)实现很易懂,就是将消息名和监听函数一一对应地存储在 scope.$$listeners对象

里,这里唯一的亮点在于 scope.$$listenerCount

的维护和用途。

当一个子作用域注册新的自定义事件时,它自身和它所有祖先作用域的 scope.$$listenerCount

都会加1,而当事件被取消时,该作用域和它所有祖先作用域的 scope.$$listenerCount

也会减1,这是一个性能优化点,当进行 scope.broadcast

传递消息(深度优先遍历)时,就无需遍历到每一个叶子作用域(即叶子节点),所以说 scope.$$listenerCount

不是指该作用域上该事件(消息)名有多少个监听函数。

$on: function(name, listener) {  var namedListeners = this.$$listeners[name];  if (!namedListeners) {    this.$$listeners[name] = namedListeners = [];  }  // 存储监听函数  namedListeners.push(listener);  var current = this; // 维护$$listenerCount,用于提高broadcast的性能  do {    if (!current.$$listenerCount[name]) {      current.$$listenerCount[name] = 0;    }    current.$$listenerCount[name]++;  } while ((current = current.$parent));  var self = this;  // 返回函数,用来取消监听函数  return function() {    namedListeners[namedListeners.indexOf(listener)] = null;    decrementListenerCount(self, 1, name);  };}
$emit向上冒泡传递事件(消息)通过作用域关系 scope.$parent

不断向父作用域传递消息,达到冒泡的效果,既然是冒泡,当然就有阻止冒泡的方法,angular在会传递给监听函数一个 event对象

,可以通过 event.stopPropagation

方法来做到这一点。

$emit: function(name, args) {  var empty = [],    namedListeners,    scope = this,    stopPropagation = false,    event = { // 传递给监听函数的event对象      name: name,      targetScope: scope,   // 目标作用域,类似于jquery中的event.target      stopPropagation: function() {        stopPropagation = true;      },      preventDefault: function() {        event.defaultPrevented = true;      },      defaultPrevented: false    },    listenerArgs = concat([event], arguments, 1),// 传递给监听函数的参数    i, length;  do { // 循环处理作用域上的监听函数    namedListeners = scope.$$listeners[name] || empty;    event.currentScope = scope; // 当前作用域,类似于jquery中的event.currentTarget    for (i = 0, length = namedListeners.length; i ##### $broadcast向下广播传递事件(消息)和    $emit

一样需要向其他作用域传递消息,这里的传递的目标作用域不再是 $parent

,而是所有的子作用域,避免深层次的循环嵌套,采用深度优先算法遍历作用域树,从而达到广播的效果,这里只看下核心代码:

$broadcast: function(name, args) {  // ... 省略定义代码  // 循环遍历所有子作用域  while ((current = next)) {    event.currentScope = current;    listeners = current.$$listeners[name] || [];    for (i = 0, length = listeners.length; i ### 最后差不多以上就是笔者对angular scope源码的一些理解,如果不对的地方,欢迎留言指正,新浪微博 &8211; Lovesueee。

关键字:AliUED, angular, scope, 前端开发


本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!

相关文章

立即
投稿

微信公众账号

微信扫一扫加关注

返回
顶部