[AngularJS面面观] 5. scope中的两个异步方法 - $applyAsync以及$evalAsync

Angular中digest循环的主干是对于watchers的若干次遍历,直到整个scope中的数据”稳定”下来,这部分实现在这篇文章中已经进行了详尽的介绍。相关的一些细节优化也在这篇文章中进行了分析。

除了主干的内容,digest循环的内容其实还包括几个比较有趣的部分,比如这一节我们即将分析的$evalAsync以及$applyAsync,下面我们就来通过相关源代码来分析一下:

$digest: function() {
  var watch, value, last, fn, get,
      watchers,
      length,
      dirty, ttl = TTL,
      next, current, target = this,
      watchLog = [],
      logIdx, logMsg, asyncTask;

  beginPhase(‘$digest‘);
  // Check for changes to browser url that happened in sync before the call to $digest
  $browser.$$checkUrlChange();

  if (this === $rootScope && applyAsyncId !== null) {
    // If this is the root scope, and $applyAsync has scheduled a deferred $apply(), then
    // cancel the scheduled $apply and flush the queue of expressions to be evaluated.
    $browser.defer.cancel(applyAsyncId);
    flushApplyAsync();
  }

  lastDirtyWatch = null;

  do { // "while dirty" loop
    dirty = false;
    current = target;

    while (asyncQueue.length) {
      try {
        asyncTask = asyncQueue.shift();
        asyncTask.scope.$eval(asyncTask.expression, asyncTask.locals);
      } catch (e) {
        $exceptionHandler(e);
      }
      lastDirtyWatch = null;
    }

    traverseScopesLoop:
    do { // "traverse the scopes" loop
      if ((watchers = current.$$watchers)) {
        // process our watches
        length = watchers.length;
        while (length--) {
          try {
            watch = watchers[length];
            // Most common watches are on primitives, in which case we can short
            // circuit it with === operator, only when === fails do we use .equals
            if (watch) {
              get = watch.get;
              if ((value = get(current)) !== (last = watch.last) &&
                  !(watch.eq
                      ? equals(value, last)
                      : (typeof value === ‘number‘ && typeof last === ‘number‘
                         && isNaN(value) && isNaN(last)))) {
                dirty = true;
                lastDirtyWatch = watch;
                watch.last = watch.eq ? copy(value, null) : value;
                fn = watch.fn;
                fn(value, ((last === initWatchVal) ? value : last), current);
                if (ttl < 5) {
                  logIdx = 4 - ttl;
                  if (!watchLog[logIdx]) watchLog[logIdx] = [];
                  watchLog[logIdx].push({
                    msg: isFunction(watch.exp) ? ‘fn: ‘ + (watch.exp.name || watch.exp.toString()) : watch.exp,
                    newVal: value,
                    oldVal: last
                  });
                }
              } else if (watch === lastDirtyWatch) {
                // If the most recently dirty watcher is now clean, short circuit since the remaining watchers
                // have already been tested.
                dirty = false;
                break traverseScopesLoop;
              }
            }
          } catch (e) {
            $exceptionHandler(e);
          }
        }
      }

      // Insanity Warning: scope depth-first traversal
      // yes, this code is a bit crazy, but it works and we have tests to prove it!
      // this piece should be kept in sync with the traversal in $broadcast
      if (!(next = ((current.$$watchersCount && current.$$childHead) ||
          (current !== target && current.$$nextSibling)))) {
        while (current !== target && !(next = current.$$nextSibling)) {
          current = current.$parent;
        }
      }
    } while ((current = next));

    // `break traverseScopesLoop;` takes us to here

    if ((dirty || asyncQueue.length) && !(ttl--)) {
      clearPhase();
      throw $rootScopeMinErr(‘infdig‘,
          ‘{0} $digest() iterations reached. Aborting!\n‘ +
          ‘Watchers fired in the last 5 iterations: {1}‘,
          TTL, watchLog);
    }

  } while (dirty || asyncQueue.length);

  clearPhase();

  while (postDigestQueue.length) {
    try {
      postDigestQueue.shift()();
    } catch (e) {
      $exceptionHandler(e);
    }
  }
}

以上就是digest方法的完整实现。有了前面的知识铺垫,我们再来阅读一下这段代码,看看是否会有新的收获。

L10-L20:

beginPhase(‘$digest‘);
// Check for changes to browser url that happened in sync before the call to $digest
$browser.$$checkUrlChange();

if (this === $rootScope && applyAsyncId !== null) {
  // If this is the root scope, and $applyAsync has scheduled a deferred $apply(), then
  // cancel the scheduled $apply and flush the queue of expressions to be evaluated.
  $browser.defer.cancel(applyAsyncId);
  flushApplyAsync();
}

首先,会使用beginPhase方法将当前的状态标注为$digest

由于URL的变化可能和$digest方法的调用同时发生,通过$browser上定义的$$checkUrlChange方法来检测一下是否发生了URL的变化:

self.$$checkUrlChange = fireUrlChange;

function fireUrlChange() {
  if (lastBrowserUrl === self.url() && lastHistoryState === cachedState) {
    return;
  }

  lastBrowserUrl = self.url();
  lastHistoryState = cachedState;
  forEach(urlChangeListeners, function(listener) {
    listener(self.url(), cachedState);
  });
}

如果URL没有发生变化那么立即返回。反之则会保存当前的URL和相关历史状态,同时调用当URL发生变化时注册过的监听器。

这部分的内容和我们这一节的内容关系并不大,以后我希望专门用一些篇幅来阐述,这里就不再深入下去。

好了,那么下面L14-L19是在做什么呢?

if (this === $rootScope && applyAsyncId !== null) {
  // If this is the root scope, and $applyAsync has scheduled a deferred $apply(), then
  // cancel the scheduled $apply and flush the queue of expressions to be evaluated.
  $browser.defer.cancel(applyAsyncId);
  flushApplyAsync();
}

这里出现了好多奇奇怪怪的东西。主要是这几个新概念:

1. $applyAsync

2. applyAsyncId以及$browser.defer.cancel

3. flushApplyAsync()

这里出现了本节的主角之一$applyAsync

2和3则是围绕它的两个概念。

来看看它的实现:

$applyAsync: function(expr) {
  var scope = this;
  expr && applyAsyncQueue.push($applyAsyncExpression);
  expr = $parse(expr);
  scheduleApplyAsync();

  function $applyAsyncExpression() {
    scope.$eval(expr);
  }
}

让我们看看这个方法的实现。首先,将传入的参数表达式通过闭包给包装到一个函数中,并将该函数置入到一个名为applyAsyncQueue的数组中。解析表达式为Angular能够辨识的形式(通过$parse服务)。最后调用scheduleApplyAsync方法:

var applyAsyncId = null;

function scheduleApplyAsync() {
  if (applyAsyncId === null) {
    applyAsyncId = $browser.defer(function() {
      $rootScope.$apply(flushApplyAsync);
    });
  }
}

需要明白的是,applyAsyncId只是定义在$rootScope上的一个变量而已。

scheduleApplyAsync方法中,会首先判断applyAsyncId是否已经被定义了,如果没有定义的话会使用$browser.defer来生成一个applyAsyncId

这里出现的$browser.defer目前不打算分析它的代码。现在需要知道的是它只不过是浏览器环境下JavaScript原生函数setTimeout的一层封装。而对应的$browser.defer.cancel(deferId)用来取消由$browser.defer定义的一个延迟执行任务。

很显然,需要调度的异步任务是:

$rootScope.$apply(flushApplyAsync);

// 下面是具体任务的定义
function flushApplyAsync() {
  while (applyAsyncQueue.length) {
    try {
      applyAsyncQueue.shift()();
    } catch (e) {
      $exceptionHandler(e);
    }
  }
  applyAsyncId = null;
}

具体执行的任务很好理解,从applyAsyncQueue这个数组中依次拿出前面置入的函数并执行。最后将applyAsyncId重置为空表明执行完毕。

初步了解了$applyAsync的实现后,再看看上面这段代码:

if (this === $rootScope && applyAsyncId !== null) {
  // If this is the root scope, and $applyAsync has scheduled a deferred $apply(), then
  // cancel the scheduled $apply and flush the queue of expressions to be evaluated.
  $browser.defer.cancel(applyAsyncId);
  flushApplyAsync();
}

正如同注释说明的那样,如果当前scope是$rootScope并且定义了需要异步执行的任务的话,取消该任务并马上执行保存在applyAsyncQueue这个数组中的每个表达式(通过flushApplyAsync方法)。

这样做的原因也比较好理解,目前已经进入了一轮digest循环,这是执行之前定义的异步任务的一个合适契机。因为$apply方法最终也会触发$digest方法的执行,那么在这里直接执行就能够减少一次不必要的digest调用。

好了,跟第一个主角$applyAsync打好招呼,下面这段代码我们会碰到第二个主角$evalAsync

在上面digest完整代码的L27-L35:

while (asyncQueue.length) {
  try {
    asyncTask = asyncQueue.shift();
    asyncTask.scope.$eval(asyncTask.expression, asyncTask.locals);
  } catch (e) {
    $exceptionHandler(e);
  }
  lastDirtyWatch = null;
}

当代码运行到这里时,事实上已经进入了digest循环体。

它遍历asyncQueue这个数组,并且通过$eval方法依次执行定义在其中的表达式。

那么数组asyncQueue是怎么什么时候被填充的呢?嗯,没错,就是在调用$evalAsync时。

$evalAsync: function(expr, locals) {
  // if we are outside of an $digest loop and this is the first time we are scheduling async
  // task also schedule async auto-flush
  if (!$rootScope.$$phase && !asyncQueue.length) {
    $browser.defer(function() {
      if (asyncQueue.length) {
        $rootScope.$digest();
      }
    });
  }

  asyncQueue.push({scope: this, expression: $parse(expr), locals: locals});
}

如果当前不处于$digest或者$apply的过程中(只有在$apply$digest方法中才会设置$$phase这个字段),并且asyncQueue数组中还不存在任务时,就会异步调度一轮digest循环来确保asyncQueue数组中的表达式会被执行。

如果没有后面这个判断条件的话,每次调用$evalAsync都会触发一次digest循环,那么会给整个应用造成不必要的负担。因为一次digest循环会执行掉asyncQueue数组中所有的任务,而多次执行显然是没有意义的,添加的负担就是触发一轮watchers中watch方法的遍历。而不可排出某些watch方法可能会相当耗时(即时在watch方法中执行耗时任务并不是一个好的实践)。

弄清楚了$evalAsync的作用,那么使用它到底又有什么意义呢?在什么场合下需要使用它?

这就涉及到了一个问题,延迟执行的时机。我们知道,当我们需要在某个”晚一点”的时候执行一段代码的时候,我们会使用setTimeout方法,或者在Angular环境中的$timeout服务。但是它们的共通之处在于它们都依赖于浏览器的事件循环机制(Event Loop)。也就是说,在我们调用了setTimeout或者$timeout后,我们将何时执行这段延时代码的控制权交给了浏览器。可是我们的浏览器大哥可是很忙的,你以为你指定了timeout时间为100毫秒,大哥就一定会在100毫秒之后执行吗?这有一点不现实。如果事件循环中存在了一些耗时任务,那么你的任务的执行时间就完全不可控了。大哥可能在执行了一堆排在你的任务之前的任务后才会来执行你的任务。这个时候也许黄花菜都凉了。

$evalAsync就尝试解决这一问题。如果目前已经处于一轮digest循环中,那么它能够确保你定义的任务在本轮digest循环期间一定会被执行!因此,这个过程和浏览器就没有任何关系了,这样能够提高浏览器的渲染效率,因为无效的渲染被屏蔽了。关于$timeout$evalAsync,在Stackoverlow上有比较好的一个总结,简单的翻译一下:

1. 如果在directive中使用$evalAsync,那么它的运行时机在Angular对DOM进行操作之后,浏览器渲染之前。

2. 如果在controller中使用$evalAsync,那么它的运行时机在Angular对DOM进行操作之前,同时也在浏览器渲染之前 - 很少需要这样做。

3. 如果通过$timeout来异步执行代码,那么它的运行时机在Angular对DOM进行操作之后,也在浏览器渲染完毕之后(这也许会造成页面闪烁)。

如果当前不在一轮digest循环中,和$timeout就几乎没有区别了。因为它会通过下面的代码触发digest循环:

$browser.defer(function() {
  if (asyncQueue.length) {
    $rootScope.$digest();
  }
});

$browser.defer等同于直接调用setTimeout

因此,我们可以记住一个结论:使用$evalAsync的最佳场合是在指令的link方法中。这样能够避免浏览器不必要的渲染而造成的页面闪烁。当你在directive中考虑使用$timeout时,不妨试试$evalAsync

因为在digest循环中引入了对asyncQueue数组的操作。循环的终止条件也需要相应作出调整:

  ...
  if ((dirty || asyncQueue.length) && !(ttl--)) {
    clearPhase();
    throw $rootScopeMinErr(‘infdig‘,
        ‘{0} $digest() iterations reached. Aborting!\n‘ +
        ‘Watchers fired in the last 5 iterations: {1}‘,
        TTL, watchLog);
  }

} while (dirty || asyncQueue.length);

不能再以是否dirty作为循环的终止条件了。考虑一种极端情况,如果watcher的watch方法中不停的调用$evalAsync,那么就会造成asyncQueue数组永远无法被执行完。因此这种情况也会触发达到最大digest数的异常。

另外,在最外层的while循环条件中,也加入了asyncQueue.length,只有当asyncQueue数组中的所有任务都完成时,才能考虑推出digest循环。

最后,如果你细心,还会发现在digest方法的最后,digest循环体之外,还有一个while循环:

while (postDigestQueue.length) {
  try {
    postDigestQueue.shift()();
  } catch (e) {
    $exceptionHandler(e);
  }
}

形式上和之前处理asyncQueue数组挺相似的,看看在什么地方操作了postDigestQueue数组就明白是啥意思了:

$$postDigest: function(fn) {
  postDigestQueue.push(fn);
}

以上是scope定义的一个方法,按照Angular的代码规约,它实际上是一个private方法,因为它的前缀有两个$符号。那么它是用来干什么的呢?从该循环的位置可以得出判断:用于在digest循环后执行,因此也可以将$$postDigest方法理解为一些callback的注册,这些callback会在digest循环完毕后被调用。尽管$$postDigest方法是一个private方法,在确实有需求在digest循环后执行某些任务时,也是可以考虑使用的。

至此,digest循环的主体部分就介绍的差不多了。其实它还涉及到了一些其他的概念,比如:

1. scope的继承机制,因为digest循环会遍历整个scope树结构。

2. watcher的watch方法如何判断scope上的某个数据是否发生了变化,判断的方式因该数据的类型而异。关于这一点再前面的文章中已经简要叙述过了,以后有空会有专门的文章再深入探讨这个问题。

时间: 2024-11-06 01:51:09

[AngularJS面面观] 5. scope中的两个异步方法 - $applyAsync以及$evalAsync的相关文章

[AngularJS面面观] 12. scope中的watch机制---第三种策略$watchCollection

如果你刚刚入门angular,你或许还在惊叹于angular的双向绑定是多么的方便,你也许在庆幸未来的前端代码中再也不会出现那么多繁琐的DOM操作了. 但是,一旦你的应用程序随着业务的复杂而复杂,你就会发现你手头的那些angular的知识似乎开始不够用了.为什么绑定的数据没有生效?为什么应用的速度越来越慢?为什么会出现莫名其妙的infinite digest异常?所以你开始尝试进阶,尝试弄清楚在数据绑定这个现象后面到底发生了什么. 相信能顺着前面数十篇文章看到这里的同学们,一定对angular是

angularjs学习笔记3-directive中scope的绑定修饰符

在angularjs中,一个directive返回一个对象,对象存在很多属性,并且可以在directive中自定义自己的scope,而使用自己的scope是为了防止一个directive被使用在多个地方之间发生相互影响,通常这个scope可以被定义为true或者对象,当这个scope被定义为true时,代表这个指令拥有独立的scope,它被多次使用在多个地方之间不会相互影响,例如: HTML代码 <!doctype html> <html ng-app="MyModule&qu

Angularjs MVC 以及 $scope 作用域 Angularjs 模块 的 run 方法 以及依赖注入中代码压缩问题

Angularjs MVC 以及 $scope 作用域 Angularjs 模块的 run 方法 以及依赖注入中代码压缩问题 1. Angularjs MVCModel:数据模型层View:视图层,负责展示Controller:业务逻辑和控制逻辑优点: 代码模块化 代码逻辑比较清晰.可移值性高,后期维护方便.代码复用,代码规模越来越大的时候,切分职责是大势所趋缺点:运行效率稍微低一些2. Angularjs $scope 作用域1. $scope 多控制器单独作用域<!DOCTYPE html>

用AngularJS开发的过程中如何查看Scope内容

scope的继承就好比JS的原型链继承,深入理解Scope:http://www.lovelucy.info/understanding-scopes-in-angularjs.html 通过查看官网文档:http://docs.angularjs.cn/guide/scope(这个链接不知道为什么直接点击会显示Not Found,首先访问http://docs.angularjs.cn/api,点击导航栏的“英文文档”在下拉菜单中选择“Developer Guide”,左边导航栏中选择“Sco

一招制敌 - 玩转 AngularJS 指令的 Scope (作用域),讲得特别好

学习了AngularJS挺长时间,最近再次回首看看指令这部分的时候,觉得比自己刚开始学习的时候理解的更加深入了,尤其是指令的作用域这部分. 步入正题: 每当一个指令被创建的时候,都会有这样一个选择,是继承自己的父作用域(一般是外部的Controller提供的作用域或者根作用域($rootScope)),还是创建一个新的自己的作用域,当然AngularJS为我们指令的scope参数提供了三种选择,分别是:false,true,{}:默认情况下是false. scope = false 首先我们来看

用angularjs在循环遍历中绑定ng-model(转载---CSDN博客 )

用angularjs在循环遍历中绑定ng-model CSDN博客 原文  http://blog.csdn.net/chen2991101/article/details/19764263 angularjs的双向绑定非常的好用,当修改了一个地方的值后另外一个地方也同步修改了,如果用平时的js来写的话需要写很多代码,但是用了angularjs后只需要几行代码就能轻松搞定. 想做一个类似于淘宝的改价的功能,就是当用户拍下了宝贝后卖家给你调价的那个功能,界面就像这样: 当修改了折扣或者直接填写了优

AngularJs学习笔记--Scope

原版地址:http://code.angularjs.org/1.0.2/docs/guide/scope 一.什么是Scope? scope(http://code.angularjs.org/1.0.2/docs/api/ng.$rootScope.Scope)是一个指向应用model的object.它也是expression(http://www.cnblogs.com/lcllao/archive/2012/09/16/2687162.html)的执行上下文.scope被放置于一个类似应

《AngularJS权威教程》中关于指令双向数据绑定的理解

在<AngularJS权威教程>中,自定义指令和DOM双向数据绑定有一个在线demo,网址:http://jsbin.com/IteNita/1/edit?html,js,output,具体代码如下: <!doctype html> <html ng-app="myApp"> <head> <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.0-

一招制敌 - 玩转 AngularJS 指令的 Scope (作用域)【转】

学习了AngularJS好长时间,最近再次回首看看指令这部分的时候,觉得比自己刚开始学习的时候理解的更加深入了,尤其是指令的作用域这部分. 当初看的是<AngularJS权威指南>这本书,但是感觉这本书关于这方面讲的不是很细致,另外吐槽一下,这本书中文版印刷的质量不是很好,很多地方都有错误:不过讲的还是可以的,是一本学习AngularJS的好书. 下面我们就来详细分析一下指令的作用域.在这之前希望你对AngularJS的Directive有一定的了解,不然你对下面部分的理解可能会有一点难度.