[AngularJS面面观] 5. scope中的两个异步方法 - $applyAsync以及
Angular中digest循环的主干是对于watchers的若干次遍历,直到整个scope中的数据”稳定”下来,这部分实现在这篇文章中已经进行了详尽的介绍。相关的一些细节优化也在这篇文章中进行了分析。 除了主干的内容,digest循环的内容其实还包括几个比较有趣的部分,比如这一节我们即将分析的 $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,then
// cancel the scheduled $apply and flush the queue of expressions to be evaluated.
$browser.defer.cancel(applyAsyncId);
flushApplyAsync();
}
首先,会使用 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,then
// cancel the scheduled $apply and flush the queue of expressions to be evaluated.
$browser.defer.cancel(applyAsyncId);
flushApplyAsync();
}
这里出现了好多奇奇怪怪的东西。主要是这几个新概念: 这里出现了本节的主角之一 来看看它的实现: $applyAsync: function(expr) {
var scope = this;
expr && applyAsyncQueue.push($applyAsyncExpression);
expr = $parse(expr);
scheduleApplyAsync();
function $applyAsyncExpression() {
scope.$eval(expr);
}
}
让我们看看这个方法的实现。首先,将传入的参数表达式通过闭包给包装到一个函数中,并将该函数置入到一个名为 var applyAsyncId = null;
function scheduleApplyAsync() {
if (applyAsyncId === null) {
applyAsyncId = $browser.defer(function() {
$rootScope.$apply(flushApplyAsync);
});
}
}
需要明白的是, 在 这里出现的 很显然,需要调度的异步任务是: $rootScope.$apply(flushApplyAsync);
// 下面是具体任务的定义
function flushApplyAsync() {
while (applyAsyncQueue.length) {
try {
applyAsyncQueue.shift()();
} catch (e) {
$exceptionHandler(e);
}
}
applyAsyncId = null;
}
具体执行的任务很好理解,从 初步了解了$applyAsync的实现后,再看看上面这段代码: if (this === $rootScope && applyAsyncId !== null) {
// If this is the root scope,then
// cancel the scheduled $apply and flush the queue of expressions to be evaluated.
$browser.defer.cancel(applyAsyncId);
flushApplyAsync();
}
正如同注释说明的那样,如果当前scope是 这样做的原因也比较好理解,目前已经进入了一轮digest循环,这是执行之前定义的异步任务的一个合适契机。因为 了解了
简单翻译一下:让 看上去有那么一点点道理,但还是有一些不明就里的感觉。但是下面这个例子就让它的用途清晰起来了。我们知道在Angular中发起AJAX请求一般通过 所以在
配置 所以,答案就比较清晰了。如果在你的应用中,经常同时调用很多个AJAX请求,那么可以考虑配置: $httpProvider.useApplyAsync(true);
好了,跟第一个主角 在上面digest完整代码的L27-L35: while (asyncQueue.length) {
try {
asyncTask = asyncQueue.shift();
asyncTask.scope.$eval(asyncTask.expression,asyncTask.locals);
} catch (e) {
$exceptionHandler(e);
}
lastDirtyWatch = null;
}
当代码运行到这里时,事实上已经进入了digest循环体。 那么数组 $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循环中,和$timeout就几乎没有区别了。因为它会通过下面的代码触发digest循环: $browser.defer(function() {
if (asyncQueue.length) {
$rootScope.$digest();
}
});
而 因此,我们可以记住一个结论:使用 因为在digest循环中引入了对 ...
if ((dirty || asyncQueue.length) && !(ttl--)) {
clearPhase();
throw $rootScopeMinErr('infdig','{0} $digest() iterations reached. Aborting!n' +
'Watchers fired in the last 5 iterations: {1}',watchLog);
}
} while (dirty || asyncQueue.length);
不能再以是否dirty作为循环的终止条件了。考虑一种极端情况,如果watcher的watch方法中不停的调用 另外,在最外层的while循环条件中,也加入了 最后,如果你细心,还会发现在digest方法的最后,digest循环体之外,还有一个while循环: while (postDigestQueue.length) {
try {
postDigestQueue.shift()();
} catch (e) {
$exceptionHandler(e);
}
}
形式上和之前处理 $$postDigest: function(fn) {
postDigestQueue.push(fn);
}
以上是scope定义的一个方法,按照Angular的代码规约,它实际上是一个private方法,因为它的前缀有两个$符号。那么它是用来干什么的呢?从该循环的位置可以得出判断:用于在digest循环后执行,因此也可以将 至此,digest循环的主体部分就介绍的差不多了。其实它还涉及到了一些其他的概念,比如: 1. scope的继承机制,因为digest循环会遍历整个scope树结构。 2. watcher的watch方法如何判断scope上的某个数据是否发生了变化,判断的方式因该数据的类型而异。关于这一点再前面的文章中已经简要叙述过了,以后有空会有专门的文章再深入探讨这个问题。 (编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |