作用域
第一章 作用域和Digest(四)
联合$apply
调用 - $applyAsync
不论在digest里面还是外面调用$evalAsync
去延迟工作,他实际是为之前的使用案例设计的。之所以在setTimeout
中调用digest是为了在digest循环外面调用$evalAsync
时防止混淆。
针对在digest循环外部异步调用$apply
的情况,同样有一个名为$applyAsync
来处理。其使用类似于$apply
- 为了集成没有意识到Angular digest循环的代码。和$apply
不同的是,他不立即计算给定的函数,也不立即发起一个digest。而是,他调度这两件事情在之后很短时间内运行。
添加$applyAsync
函数的原始目的是:处理HTTP相应。每当$http
服务接受到响应,任何相应程序都会被调用,同时调用了digest。这意味着对每一个HTTP相应都会有一个digest运行。对于有很多HTTP流量的应用程序(例如很多应用在启动的时候),可能存在性能问题,或者是很大代价的digest循环。现在$http
服务可以使用$applyAsync
来配置,针对HTTP相应到达的时间非常相近的情况,他们会被集成到同一个digest循环中。然而,$applyAsync
不仅尝试解决$http
服务,你也可以在联合使用digest循环有利的情况下来使用它。
正如在下面第一个测试案例中看到的,当我们$applyAsync
一个函数时,他不会立即被调用,而是在50毫秒后被调用:
test/scope_spec.js
it("allows async $apply with $applyAsync", function(done){
scope.counter = 0;
scope.$watch(
function(scope){
return scope.aValue;
},
function(newValue, oldValue, scope){
scope.counter ++;
}
);
scope.$digest();
expect(scope.counter).toBe(1);
scope.$applyAsync(function(scope){
scope.aValue = ‘abc‘;
});
expect(scope.counter).toBe(1);
setTimeout(function() {
expect(scope.counter).toBe(2);
done;
}, 50);
});
到现在为止他和$evalAsync
没有任何不同,但是当我们在监听函数中调用$applyAsync
时我们开始看到不同。如果我们使用$evalAsync
,该函数会在同一个digest中被调用。但是$applyAsync
永远推迟调用:
test/scope_spec.js
it("never executes $applyAsync‘ed function in the same cycle", function(done){
scope.aValue = [1, 2, 3];
scope.asyncApplied = false;
scope.$watch(
function(scope) {
return scope.aValue;
},
function(newValue, oldValue, scope){
scope.$applyAsync(function(scope){
scope.asyncApplied = true;
});
}
);
scope.$digest();
expect(scope.asyncApplied).toBe(false);
setTimeout(function(){
expect(scope.asyncApplied).toBe(true);
done();
}, 50);
});
让我们通过在Scope构造函数中引入另一个队列来实现$applyAsync
。
src/scope.js
function Scope(){
this.$$watchers = [];
this.$$lastDirtyWatch = null;
this.$$asyncQueue = [];
this.$$applyAsyncQueue = [];
this.$$phase = null;
}
当调用$applyAsync
时,我们将该函数放入队列中。和$apply
类似,函数将会在不久之后在当前scope的上下文中计算给定的表达式:
src/scope.js
Scope.prototype.$applyAsync = function(expr){
var self = this;
self.$$applyAsyncQueue.push(function(){
self.$eval(expr);
});
};
我们在这里还应该做的是调度函数应用。我们可以使用setTimeout
延时0毫秒。在延时中,我们$apply
从队列中取出的每一个函数并调用所有函数:
src/scope.js
Scope.prototype.$applyAsync = function(expr){
var self = this;
self.$$applyAsyncQueue.push(function(){
self.$eval(expr);
});
setTimeout(function(){
self.$apply(function(){
while(self.$$applyAsyncQueue.length){
self.$$applyAsyncQueue.shift()();
}
});
}, 0);
};
注意:我们不会
$apply
队列中的每一个元素。我们只在循环的外面$apply
一次。这里我们只希望一次digest循环。
正如我们所讨论的,$applyAsync
最主要的是优化快速发生的一系列事情使其能够在一个digest中完成。我们还没有完成这个目标。每次调用$applyAsync
都会调度一个新的digest,如果我们在监控函数中添加一个计数器,这将很明白的看到这点:
test/scope_spec.js
it("coalesces many calls to $applyAsync", function(done){
scope.counter = 0;
scope.$watch(
function(scope) {
scope.counter ++;
return scope.aValue;
},
function(newValue, oldValue, scope){}
);
scope.$applyAsync(function(scope){
scope.aValue = ‘abc‘;
});
scope.$applyAsync(function(scope){
scope.aValue = ‘def‘;
});
setTimeout(function() {
expect(scope.counter).toBe(2);
done();
}, 50);
});
我们希望计数器的值是2(监控在第一次digest中被执行了两次),而不是超过2。
我们需要做的是追踪在setTimeout
遍历队列的过程是否被调度了。我们将该信息放在Scope的一个私有属性上,名叫$$applyAsyncId
:
src/scope.js
function Scope(){
this.$$watchers = [];
this.$$lastDirtyWatch = null;
this.$$asyncQueue = [];
this.$$applyAsyncQueue = [];
this.$$applyAsyncId = null;
this.$$phase = null;
}
当我们调度任务时,我们先要检查该属性,并在任务被调度的过程中保持其状态,直到结束。
src/scope.js
Scope.prototype.$applyAsync = function(expr){
var self = this;
self.$$applyAsyncQueue.push(function(){
self.$eval(expr);
});
if(self.$$applyAsyncId === null){
self.$$applyAsyncId = setTimeout(function(){
self.$apply(function(){
while(self.$$applyAsyncQueue.length){
self.$$applyAsyncQueue.shift()();
}
self.$$applyAsyncId = null;
});
}, 0);
}
};
译者注:有人可能不明白这个解决方案。请注意:当在setTimeout中传入的延时参数为0时,在当前调用setTimeout进程结束之前,setTimeout里面的函数不会被执行。在测试案例中调用了两次
$applyAsync
,但是setTimeout不会执行,直到执行了测试案例中最后一行的setTimeout,然后根据setTimeout中的延时执行setTimeout中的函数。由于第二次调用$applyAsync
时,$$applyAsyncId不为空,所以不会再次设置一个setTimeout,最终该测试案例中有两个setTimeout,根据时间先后,$applyAsync
中的会先运行。
关于$$applyAsyncId
的另一方面是,如果在超时被触发之前,有digest因为某些原因被发起,那么他不应该再次发起一个digest。在这种情况下,digest应该遍历队列,同时$applyAsync
应该被取消:
test/scope_spec.js
it(‘cancels and flushed $applyAsync if digested first‘, function(done){
scope.counter = 0;
scope.$watch(
function(scope) {
scope.counter ++;
return scope.aValue;
},
function(newValue, oldValue, scope) {}
);
scope.$applyAsync(function(scope){
scope.aValue = ‘abc‘;
});
scope.$applyAsync(function(scope){
scope.aValue = ‘def‘;
});
scope.$digest();
expect(scope.counter).toBe(2);
expect(scope.aValue).toEqual(‘def‘);
setTimeout(function(){
expect(scope.counter).toBe(2);
done();
}, 50);
});
这里我们测试了如果我们调用了$digest
,使用$applyAsync
调度的每一个任务都会立即执行。不会留下任务以后执行。
让我们先来提取在$applyAsync
内部要使用的清空队列的函数,这样我们就能在很多地方调用它:
src/scope.js
Scope.prototype.$$flushApplyAsync = function() {
while (this.$$applyAsyncQueue.length){
this.$$applyAsyncQueue.shift()();
}
this.$$applyAsyncId = null;
};
src/scope.js
Scope.prototype.$applyAsync = function(expr){
var self = this;
self.$$applyAsyncQueue.push(function(){
self.$eval(expr);
});
if(self.$$applyAsyncId === null){
self.$$applyAsyncId = setTimeout(function(){
self.$apply(_.bind(self.$$flushApplyAsync, self));
}, 0);
}
};
LoDash _.bind
函数和 ECMAScript 5Function.prototype.bind
函数等价,被用来确定接受函数的this
是一个已知值。
现在我们可以在$digest
中调用该函数 - 如果存在$applyAsync
挂起,我们取消它,并且立即清空任务:
src/scope.js
Scope.prototype.$digest = function(){
var tt1 = 10;
var dirty;
this.$$lastDirtyWatch = null;
this.$beginPhase("$digest");
if(this.$$applyAsyncId){
clearTimeout(this.$$applyAsyncId);
this.$$flushApplyAsync();
}
do {
while (this.$$asyncQueue.length){
var asyncTask = this.$$asyncQueue.shift();
asyncTask.scope.$eval(asyncTask.expression);
}
dirty = this.$$digestOnce();
if((dirty || this.$$asyncQueue.length) && !(tt1 --)) {
this.$clearPhase();
throw ‘10 digest iterations reached‘;
}
} while (dirty || this.$$asyncQueue.length);
this.$clearPhase();
};
这是$applyAsync
所有的内容。在你知道你在很短的时间内多次使用$apply
的情况下,这是一个稍微有用的优化。
在Digest之后执行代码 - $$postDigest
还有另外一种方式在digest循环中添加一些代码去运行,是使用$$postDigest
函数。
在函数名称之前的两个$符号意味着是给Angular内部使用的函数,而不是开发者能够调用的函数。但是,在这里,我们也要实现它。
和$evalAsync
和 $applyAsync
类似,$$postDigest
调度函数之后运行。特别的是,函数会在下一次digest之后运行。和$evalAsync
相似的是,使用$$postDigest
调度的函数只会运行一次。和$evalAsync
和 $applyAsync
都不一样的是,调度一个$postDigest
函数并不会引起一个digest被调度,所以函数被延迟执行,直到digest因为某些原因发生。下面有一个满足了这个要求的单元测试:
test/scope_spec.js
it(‘runs a $$postDigest function after each digest‘, function(){
scope.counter = 0;
scope.$$postDigest(function(){
scope.counter++;
});
expect(scope.counter).toBe(0);
scope.$digest();
expect(scope.counter).toBe(1);
scope.$digest();
expect(scope.counter).toBe(1);
});
正如其名字表达的一样,$postDigest
函数在digest之后运行,因此如果你使用$postDigest
来改变作用域,他们不会立即被脏值检查机制发觉。如果你想要被发觉,你可以手动调用$digest
和或者$apply
:
test/scope_spec.js
it("doest not include $$postDigest in the digest", function(){
scope.aValue = ‘original value‘;
scope.$$postDigest(function() {
scope.aValue = ‘changed value‘;
});
scope.$watch(
function(scope){
return scope.aValue;
},
function(newValue, oldValue, scope){
scope.watchedValue = newValue;
}
);
scope.$digest();
expect(scope.watchedValue).toBe("original value");
scope.$digest();
expect(scope.watchedValue).toBe("changed value");
});
为了实现$postDigest
,让我们在Scope的构造函数中再初始化一个数组:
src/scope.js
function Scope(){
this.$$watchers = [];
this.$$lastDirtyWatch = null;
this.$$asyncQueue = [];
this.$$applyAsyncQueue = [];
this.$$applyAsyncId = null;
this.$$postDigestQueue = [];
this.$$phase = null;
}
下面,让我们实现$postDigest
本身。他所做的所有事情就是将给定的函数添加到该队列中:
src/scope.js
Scope.prototype.$$postDigest = function(fn){
this.$$postDigestQueue.push(fn);
};
最后,在$digest
中,让我们一次取出队列中的函数,并在digest完成后调用他们:
src/scope.js
Scope.prototype.$digest = function(){
var tt1 = 10;
var dirty;
this.$$lastDirtyWatch = null;
this.$beginPhase("$digest");
if(this.$$applyAsyncId){
clearTimeout(this.$$applyAsyncId);
this.$$flushApplyAsync();
}
do {
while (this.$$asyncQueue.length){
var asyncTask = this.$$asyncQueue.shift();
asyncTask.scope.$eval(asyncTask.expression);
}
dirty = this.$$digestOnce();
if((dirty || this.$$asyncQueue.length) && !(tt1 --)) {
this.$clearPhase();
throw ‘10 digest iterations reached‘;
}
} while (dirty || this.$$asyncQueue.length);
this.$clearPhase();
while(this.$$postDigestQueue.length){
this.$$postDigestQueue.shift()();
}
};
我们使用Array.shift()
方法从队列的开始消耗该队列,直到为空,并且立即执行这些函数。$postDigest
函数没有任何参数。
处理异常
我们的Scope
的实现正变得越来越像Angular的。然后,他很脆弱。这主要是因为我们并没有在异常处理上投入太多想法。
如果在一个监控函数中发生异常,任何一个$evalAsync
或者$applyAsync
或者$$postDigest
函数,还有我们当前的实现都会出错并且停止他正在做的事情。然而,Angular的实现,比我们的更加健壮。在异常抛出之前或者digest捕捉到异常都会记录下来,然后操作会再停止的地方重新开始。
Angular实际上使用一个名叫
$exceptionHandler
的服务来处理异常。因为我们现在还没有这个服务,我们现在只在控制台简单的打印异常信息。
在watch中,有两个地方可能发生异常:在监控函数中和在监听函数中。不论哪种情况,我们都希望打印出异常,并且当做什么事情都没有发生去执行下一个watch。针对这两种情况,下面有两个测试案例:
test/scope_spec.js
it("cathes exceptions in watch functions and continues", function(){
scope.aValue = ‘abc‘;
scope.counter = 0;
scope.$watch(
function(scope) { throw "error"; },
function(newValue, oldValue, scope){
scope.counter ++;
}
);
scope.$watch(
function(scope) { return scope.aValue;},
function(newValue, oldValue, scope) {
scope.counter ++;
}
);
scope.$digest();
expect(scope.counter).toBe(1);
});
it("catches exceptions in listener functions and continues", function(){
scope.aValue = ‘abc‘;
scope.counter = 0;
scope.$watch(
function(scope) { return scope.aValue;},
function(newValue, oldValue, scope){
throw "Error";
}
);
scope.$watch(
function(scope) { return scope.aValue; },
function(newValue, oldValue, scope) {
scope.counter ++;
}
);
scope.$digest();
expect(scope.counter).toBe(1);
});
在上两个案例中,我们定义了两个监控,第一个监控都抛出了一个异常。我们检查第二个监控是否能被执行。
要让这两个测试案例通过,我们需要去修改$$digestOnce
函数,并用try...catch
来包装每个监控函数的执行:
src/scope.js
Scope.prototype.$$digestOnce = function(){
var self = this;
var newValue, oldValue, dirty;
_.forEach(this.$$watchers, function(watcher){
try{
newValue = watcher.watchFn(self);
oldValue = watcher.last;
if(!(self.$$areEqual(newValue, oldValue, watcher.valueEq))){
self.$$lastDirtyWatch = watcher;
watcher.last = watcher.valueEq ? _.cloneDeep(newValue) : newValue;
watcher.listenerFn(newValue,
(oldValue === initWatchVal ? newValue: oldValue),
self);
dirty = true;
}else if(self.$$lastDirtyWatch === watcher){
return false;
}
} catch (e){
console.error(e);
}
});
return dirty;
};
$evalAsync
、$applyAsync
和$$postDigest
同样需要异常处理。他们都是用来执行和digest循环相关的任意函数。我们不希望他们中的任意一个都能导致循环永远停止。
对于$evalAsync
,我们可以定义一个测试案例,来检查即使$evalAsync
调度的任何一个函数抛出异常,监控函数仍然会继续运行:
test/scope_spec.js
it("catches exceptions in $evalAsync", function(done){
scope.aValue = ‘abc‘;
scope.counter = 0;
scope.$watch(
function(scope) { return scope.aValue; },
function(newValue, oldValue, scope){
scope.counter ++;
}
);
scope.$evalAsync(function(scope){
throw "Error";
});
setTimeout(function(){
expect(scope.counter).toBe(1);
done();
}, 50);
});
针对$applyAsync
,我们定义一个测试案例,来检查即使有一个函数在在$applyAsync
调度的函数之前抛出异常,$applyAsync
仍然能被调用:
test/scope_spec.js
it("catches exceptions in $applyAsync", function(done) {
scope.$applyAsync(function(scope) {
throw "Error";
});
scope.$applyAsync(function(scope){
throw "Error";
});
scope.$applyAsync(function(scope){
scope.applied = true;
});
setTimeout(function(){
expect(scope.applied).toBe(true);
done();
}, 50);
});
这里我们使用了两个抛出异常的函数,如果我们仅仅使用一个,第二个函数实际上会运行。这是因为
$apply
调用了$digest
,在$apply
的finally
块中$applyAsync
队列已经被消耗完了。
针对$$postDigest
,digest已经运行完了,所以没有必要在监控函数中测试它。我们可以使用第二个$$postDigest
函数来代替,确保它同样执行了:
test/scope_spec.js
it("catches exceptions in $$postDigest", function() {
var didRun = false;
scope.$$postDigest(function() {
throw "Error";
});
scope.$$postDigest(function() {
didRun = true;
});
scope.$digest();
expect(didRun).toBe(true);
});
对于$evalAsync
和$$postDigest
的修改包含在$digest
函数修改中。在这两种情况下,我们使用try...catch
封装函数的运行:
src/scope.js
Scope.prototype.$digest = function(){
var tt1 = 10;
var dirty;
this.$$lastDirtyWatch = null;
this.$beginPhase("$digest");
if(this.$$applyAsyncId){
clearTimeout(this.$$applyAsyncId);
this.$$flushApplyAsync();
}
do {
while (this.$$asyncQueue.length){
try{
var asyncTask = this.$$asyncQueue.shift();
asyncTask.scope.$eval(asyncTask.expression);
} catch(e){
console.error(e);
}
}
dirty = this.$$digestOnce();
if((dirty || this.$$asyncQueue.length) && !(tt1 --)) {
this.$clearPhase();
throw ‘10 digest iterations reached‘;
}
} while (dirty || this.$$asyncQueue.length);
this.$clearPhase();
while(this.$$postDigestQueue.length){
try{
this.$$postDigestQueue.shift()();
}catch (e){
console.error(e);
}
}
};
修改$applyAsync
,在另一方面,也就是修改把队列清空的函数$$flushApplyAsync
:
src/scope.js
Scope.prototype.$$flushApplyAsync = function() {
while (this.$$applyAsyncQueue.length){
try{
this.$$applyAsyncQueue.shift()();
}catch (e){
console.error(e);
}
}
this.$$applyAsyncId = null;
};
现在当遇到异常的时候,我们的digest循环比之前健壮多了。
扫一扫,更多关于Angular的资讯: