angularJs $evalAsync , $ $phase和$ $postDigest深入分析

上一篇文章已经详细讲解了脏检查的实现原理,如果不了解的可以点击这里
今天我们来谈下 scope中的一个异步方法$evalAsync,我平时用的很少,几乎都不怎么用,那是因为我不知道这个方法有啥作用,下面我来从前入深的介绍下这个方法:

在JavaScript中,我们经常会把一段代码“延迟”执行,把它的执行延迟到当前的执行上下文结束之后的未来某个时间点。最常见的方式就是调用setTimeout()函数或者angular的$timeout服务,里面写个时间来达到延长的效果,假如这个时间是1000ms,那这个方法就1000ms之后执行,其实不是的,它们都依赖于浏览器的事件循环机制(Event Loop),在我们调用了setTimeout或者$timeout后,我们将何时执行这段延时代码的控制权交给了浏览器。可是我们的浏览器大哥可是很忙的,你以为你指定了timeout时间为1000毫秒,大哥就一定会在1000毫秒之后执行吗?这有一点不现实。如果事件循环中存在了一些耗时任务,那么你的任务的执行时间就完全不可控了。大哥可能在执行了一堆排在你的任务之前的任务后才会来执行你的任务。这个时候也许是N个1000ms了。但是$evalAsync就能解决这一问题,我们可以来模仿下$evalAsync的底层的代码实现,然后我们再看源码就很简单了!

$evalAsync的代码实现
1.我们首先需要的是存储$evalAsync列入计划的任务,可以在Scope构造函数中初始化一个数组来做这事:
function Scope() {
  this.$$watchers = [];
  this.$$asyncQueue = [];
}
2.我们再来定义$evalAsync,它添加将在这个队列上执行的函数,我们在放入队列的对象上传入当前作用域,是为了获取当前的作用域:
Scope.prototype.$evalAsync = function(expr) {
  this.$$asyncQueue.push({scope: this, expression: expr});
};
3.然后我们在$digest中要做的第一件事就是从队列中取出每个东西,然后使用$eval来触发所有被延迟执行的函数(这里不知道$digest请看上一篇文章):
Scope.prototype.$digest = function() {
  var ttl = 10;//最大循环次数
  var dirty;
  do {
  //先看下this.$$asyncQueue里面有没有值
  //(如果是在监听函数写的$evalAsync,这里开始肯定是没有值的,只有执行第2次$digest才会有值)
  //(如果是在脏检查之前写的$evalAsync,这里开始是有值)
    while (this.$$asyncQueue.length) {
    //这里数组中的事件会全部执行,执行一个删除一个
      var asyncTask = this.$$asyncQueue.shift();
      this.$eval(asyncTask.expression);
    }
    //执行脏检查
    dirty = this.$$digestOnce();
    if (dirty && !(ttl--)) {
    //最大值是10次,当ttl为0时抛出异常
      throw "10 digest iterations reached";
    }
  } while (dirty);
};

这个实现保证了:如果当作用域还是脏的,那这个函数会在稍后执行,但还处于同一个digest中,在当前正持续的digest中或者下一次digest之前执行(上面代码注释写的很清楚了)。举例来说,你可以在一个监听器的监听函数中延迟执行一些代码(只有触发了脏检查才会执行监听函数),即使它已经被延迟了,仍然会在现有的digest遍历中被执行!因此,这个过程和浏览器就没有任何关系了,这样能够提高浏览器的渲染效率,因为无效的渲染被屏蔽了。关于$timeout和$evalAsync,在Stackoverlow上有比较好的一个总结,简单的翻译一下:

  1. 如果在directive中使用$evalAsync,那么它的运行时机在Angular对DOM进行操作之后,浏览器渲染之前。
  2. 如果在controller中使用$evalAsync,那么它的运行时机在Angular对DOM进行操作之前,同时也在浏览器渲染之前 - 很少需要这样做。
  3. 如果通过$timeout来异步执行代码,那么它的运行时机在Angular对DOM进行操作之后,也在浏览器渲染完毕之后(这也许会造成页面闪烁)。
4.需要有一种机制让$evalAsync来检测某个$digest是否已经在运行了,因为不想影响到被列入计划将要执行的那个。所以我们需要一个私有变量$$phase(初始化为null)来判断$digest是否已经在运行 ,它就是作用域上一个简单的字符串属性,存储了现在正在做的信息。
function Scope() {
  this.$$watchers = [];
  this.$$asyncQueue = [];
  this.$$phase = null;
}
5.然后,我们定义一些方法用于控制这个阶段变量:一个用于设置,一个用于清除,也加个额外的检测,以确保不会把已经激活状态的阶段再设置一次:
Scope.prototype.$beginPhase = function(phase) {
  if (this.$$phase) {
    throw this.$$phase + ' already in progress.';
  }
  this.$$phase = phase;
};
 
Scope.prototype.$clearPhase = function() {
  this.$$phase = null;
};
6.在$digest方法里,我们来从外层循环设置阶段属性为“$digest”:
Scope.prototype.$digest = function() {
  var ttl = 10;
  var dirty;
  this.$beginPhase("$digest");
  do {
    while (this.$$asyncQueue.length) {
      var asyncTask = this.$$asyncQueue.shift();
      this.$eval(asyncTask.expression);
    }
    dirty = this.$$digestOnce();
    if (dirty && !(ttl--)) {
      this.$clearPhase();
      throw "10 digest iterations reached";
    }
  } while (dirty);
  this.$clearPhase();
};
7.最终,把$$phase放进$evalAsync,它会检测作用域上现有的阶段变量,如果没有(也没有已列入计划的异步任务),就把这个digest列入计划:
Scope.prototype.$evalAsync = function(expr) {
  var self = this;
  if (!self.$$phase && !self.$$asyncQueue.length) {
    setTimeout(function() {
      if (self.$$asyncQueue.length) {
        self.$digest();
      }
    }, 0);
  }
  self.$$asyncQueue.push({scope: self, expression: expr});
};

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

$$postDigest的代码实现:

就像$evalAsync一样,$ $ postDigest也能把一个函数列入计划,让它以后运行,这个函数将在下一次digest完成之后运行,所以 $ p o s t D i g e s t 函 数 是 在 d i g e s t 之 后 运 行 的 , 如 果 你 在 postDigest函数是在digest之后运行的,如果你在 postDigestdigest $ postDigest里面修改了数据,需要手动调用$digest或者$apply,以确保这些变更能够渲染到页面上。接下来我们来模拟下代码的实现。

1.我们给Scope的构造函数加队列,这个队列给$$postDigest函数用
function Scope() {
  this.$$watchers = [];
  this.$$asyncQueue = [];
  this.$$postDigestQueue = [];
  this.$$phase = null;
}
2.我们把$$postDigest也加上去,它所做的就是把给定的函数加到队列里
Scope.prototype.$$postDigest = function(fn) {
  this.$$postDigestQueue.push(fn);
};
3.当$digest完成之后,就把队列里面的函数都执行掉
Scope.prototype.$digest = function() {
  var ttl = 10;
  var dirty;
  this.$beginPhase("$digest");
  do {
    while (this.$$asyncQueue.length) {
      var asyncTask = this.$$asyncQueue.shift();
      this.$eval(asyncTask.expression);
    }
    dirty = this.$$digestOnce();
    if (dirty && !(ttl--)) {
      this.$clearPhase();
      throw "10 digest iterations reached";
    }
  } while (dirty);
  this.$clearPhase();
 
  while (this.$$postDigestQueue.length) {
    this.$$postDigestQueue.shift()();
  }
};

到这里代码就模拟的差不多,只是实现了基本原理,比angular源码还是简陋多了,angular里面做了很多优化和异常处理,逻辑也比这复杂多了,我们只不过是模拟一个最简单的实现过程。现在我们来看下angular这部分的源码,看是不是很好理解了:

这里是注册异步队列部分

  var $rootScope = new Scope();

  //The internal queues. Expose them on the $rootScope for debugging/testing purposes.
  var asyncQueue = $rootScope.$$asyncQueue = [];
  var postDigestQueue = $rootScope.$$postDigestQueue = [];

这里是方法部分

   $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,
           fn: $parse(expr),
           locals: locals
         });
       },

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

这里是设置检测$digest的变量,和清除变量的方法

  function beginPhase(phase) {
    if ($rootScope.$$phase) {
         throw $rootScopeMinErr('inprog', '{0} already in progress', $rootScope.$$phase);
       }

       $rootScope.$$phase = phase;
     }

     function clearPhase() {
       $rootScope.$$phase = null;
     }

这里是$digest的源码 :

 $digest: function () {
            var watch, value, last, fn, get,
              watchers,
              dirty, ttl = TTL,
              next, current, target = asyncQueue.length ? $rootScope : this,
              watchLog = [],
              logIdx, 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指向当前对象
              current = target;

              // It's safe for asyncQueuePosition to be a local variable here because this loop can't
              // be reentered recursively. Calling $digest from a function passed to $evalAsync would
              // lead to a '$digest already in progress' error.
              //   循环添加异步队列
              for (var asyncQueuePosition = 0; asyncQueuePosition < asyncQueue.length; asyncQueuePosition++) {
                try {
                  asyncTask = asyncQueue[asyncQueuePosition];
                  fn = asyncTask.fn;
                  fn(asyncTask.scope, asyncTask.locals);
                } catch (e) {
                  $exceptionHandler(e);
                }
                lastDirtyWatch = null;
              }
              asyncQueue.length = 0;

              traverseScopesLoop:
                //scope循环保证所有的model都能被遍历到
                do { // "traverse the scopes" loop
                  if ((watchers = !current.$$suspended && current.$$watchers)) {
                    // process our watches
                    //循环当前scope中的每个watchers
                    watchers.$$digestWatchIndex = watchers.length;
                    while (watchers.$$digestWatchIndex--) {
                      try {
                        //遍历到的当前watch
                        watch = watchers[watchers.$$digestWatchIndex];
                        // 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;
                          // watch.get(current)获取当前最新值, last = watch.last旧值
                          if ((value = get(current)) !== (last = watch.last) &&
                            //watch.eq ?作用是判断当前被监听的元素是否是对象,当不是对象且是number类型r时排除NaN!==NaN的判断,因为该判断会返回true 
                            !(watch.eq ?
                              //equals深度对比两个对象是否相等
                              equals(value, last) :
                              (isNumberNaN(value) && isNumberNaN(last)))) {
                            //不相等则认为是脏值
                            dirty = true;
                            lastDirtyWatch = watch;
                            //watch.eq ?作用是判断当前监听的元素是否是对象
                            //如果不是对象,直接把数值更新到watch.last//如果是对象,则通过深度拷贝该对象到watch.last中
                            watch.last = watch.eq ? copy(value, null) : value;
                            fn = watch.fn;
                            //此处正式调用监听函数
                            fn(value, ((last === initWatchVal) ? value : last), current);
                            // watchLog记录日志//ttl最大是10,此处记录最后五条记录
                            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
                              });
                            }
                            // lastDirtyWatch上一轮循环中最后一个发生值变化的监听
                            //如果当前watch没有变化,且当前watch正是上一轮的最后一个变化的watch,说明此次循环中所有的脏值都已更新,循环可以结束。
                          } 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
                  // (though it differs due to having the extra check for $$suspended and does not
                  // check $$listenerCount)
                  if (!(next = ((!current.$$suspended && 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
// TTL默认最大是10,当超过10个digest时会报错,若此时仍存在脏值,则抛出异常并提醒最后五条检测记录
              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);
              }
            //当不存在dirty(也就是脏值)并且当且$digest队列(该队列最大长度为10)为0了就终止循环
            } while (dirty || asyncQueue.length);
           //beginPhase表示这一段功能开始,对应着clearPhase表示结束
            clearPhase();

            // postDigestQueuePosition isn't local here because this loop can be reentered recursively.
            while (postDigestQueuePosition < postDigestQueue.length) {
              try {
                postDigestQueue[postDigestQueuePosition++]();
              } catch (e) {
                $exceptionHandler(e);
              }
            }
            postDigestQueue.length = postDigestQueuePosition = 0;

            // Check for changes to browser url that happened during the $digest
            // (for which no event is fired; e.g. via `history.pushState()`)
            $browser.$$checkUrlChange();
          }

这里我们只需要看 $evalAsync那部分的实现,其他部分上篇文章已经详细讲了:

for (var asyncQueuePosition = 0; asyncQueuePosition < asyncQueue.length; asyncQueuePosition++) {
                try {
                  asyncTask = asyncQueue[asyncQueuePosition];
                  fn = asyncTask.fn;
                  fn(asyncTask.scope, asyncTask.locals);
                } catch (e) {
                  $exceptionHandler(e);
                }
                lastDirtyWatch = null;
              }
//下面就开始进行脏检查了

要注意执行顺序,$evalAsync是先加异步队列,当有脏检查会先判断异步队列里面有没有任务(有任务就执行),然后监听到值有无变化,有的变化的话执行监听函数,此时dirty = true,为了确保所有的值都有正常更新,外层再执行一次或者多次脏检查(最多10次,超过10次,抛出异常,给出最后5条的报错信息),进入脏检查又会先判断异步队列里面有没有任务(有任务就执行),一直这样循环下去,直到dirty = false结束循环 。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值