创建自己的AngularJS - 作用域和Digest(四)
2016-04-11 10:15
706 查看
作用域
第一章 作用域和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 5
Function.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的资讯: