码迷,mamicode.com
首页 > Web开发 > 详细

创建自己的AngularJS - 作用域和Digest(四)

时间:2016-04-11 12:23:24      阅读:116      评论:0      收藏:0      [点我收藏+]

标签:

作用域

第一章 作用域和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,在$applyfinally块中$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的资讯:
技术分享

创建自己的AngularJS - 作用域和Digest(四)

标签:

原文地址:http://blog.csdn.net/fangjuanyuyue/article/details/51119445

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!