您的位置:首页 > Web前端 > AngularJS

创建自己的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的资讯:

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: