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

作用域

第一章 作用域和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的资讯:

时间: 2024-10-15 20:40:20

创建自己的AngularJS - 作用域和Digest(四)的相关文章

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

作用域 第一章 作用域和Digest(五) 销毁监控 当你注册一个监控,很多时候你想让它和scope一样保持活跃的状态,所以不必显示的删除他.然而,有些情况下,你需要销毁一个特定的监控,但是仍然保持作用域可操作.意思就是,我们需要给监控增加一个删除操作. Angular实现这个的方式特别聪明:Angular中的$watch函数有一个返回值.他是一个函数,但其被调用的时候,即删除了其注册的监控.如果想要能够移除一个监控,只要存储注册监控函数时返回的函数,然后当不需要监控的时候调用它即可: test

构建自己的AngularJS - 作用域和Digest(一)

作用域 第一章 作用域和Digest(一) Angular作用域是简单javascript对象,因此你可以像对其他对象一样添加属性.然而,他们也有一些额外的功能,用于观测数据结构的变化.这种观察能力是使用脏值检查在digest循环中运行来实现的.这就是我们这一章将要实现的内容. 作用域对象 Scope的创建是通过在Scope构造函数之前加入new关键字来创建的.这样会产生一个简单javascript对象.让我们先来创建一个单元测试.(测试驱动开发,先写测试案例) 对Scope创建一个测试文件te

创建自己的AngularJS - 作用域继承(一)

作用域 作用域继承(一) Angular作用域继承机制直接建立在Javascript原型继承基础上,并在其根部加入了一些内容.这意味着当你理解了Javascript原型链后,将对Angular作用域继承有深入了解. 根作用域 到目前为止,我们一直在和一个作用域对象打交道,该作用域使用Scope构造函数创建: var scope = new Scope(); 根作用域就是这样创建的.之所以称之为根作用域,是因为他没有父作用域,它是典型的一个由所有作用域组成的树的根. 在现实中,你永远不用用这种方式

构建自己的AngularJS - 作用域和Digest(二)

作用域 第一章 作用域和Digest(二) 放弃一个不稳定的Digest 在我们当前的实现中有一个明显的遗漏:如果发生了两个监控函数互相监控对方的变化的情况会如何?也就是,万一状态永远不能稳定呢?就像下面的测试案例展示的情况: test/scope_spec.js it("gives up on the watchers after 10 iterations", function(){ scope.counterA = 0; scope.counterB = 0; scope.$wa

构建自己的AngularJS - 作用域和Digest(三)

作用域 第一章 作用域和Digest(三) $eval - 在当前作用域的上下文中执行代码 Angular有多种方式让你在当前作用域的上下文中执行代码.最简单的是$eval.传入一个函数当做其参数,然后将当前的作用域作为参数传给该函数,并执行它.然后它返回该函数的执行结果.$eval还有第二个可选的参数,它仅仅是被传递给将要执行的函数. 有几个单元测试展示了我们如何使用$eval: test/scope_spec.js it("execute $eval'ed function and retu

构建自己的AngularJS,第一部分:作用域和digest 转摘:http://www.ituring.com.cn/article/39865

构建自己的AngularJS,第一部分:Scope和Digest 原文链接:http://teropa.info/blog/2013/11/03/make-your-own-angular-part-1-scopes-and-digest.html Angular是一个成熟和强大的JavaScript框架.它也是一个比较庞大的框架,在熟练掌握之前,需要领会它提出的很多新概念.很多Web开发人员涌向Angular,有不少人面临同样的障碍.Digest到底是怎么做的?定义一个指令(directive

AngularJS作用域

AngularJS作用域 一.概要 在AngularJS中,子作用域(child scope)基本上都要继承自父作用域(parent scope). 但,事无绝对,也有特例,那就是指令中scope设置项为对象时,即scope:{…},这将会让指令创建一个并不继承自父作用域的子作用域,我们称之为隔离作用域(isolated scope). 指令中的scope一共可以有三个值,下面我们再来温习下: 指令之scope scope: false 默认值,指令不会新建一个作用域,使用父级作用域. scop

剖析AngularJS作用域

一.概要 在AngularJS中,子作用域(child scope)基本上都要继承自父作用域(parent scope). 但,事无绝对,也有特例,那就是指令中scope设置项为对象时,即scope:{…},这将会让指令创建一个并不继承自父作用域的子作用域,我们称之为隔离作用域(isolated scope). 指令中的scope一共可以有三个值,下面我们再来温习下: 指令之scope scope: false 默认值,指令不会新建一个作用域,使用父级作用域. scope: true 指令会创建

深入理解javascript作用域系列第四篇——块作用域

× 目录 [1]let [2]const [3]try 前面的话 尽管函数作用域是最常见的作用域单元,也是现行大多数javascript最普遍的设计方法,但其他类型的作用域单元也是存在的,并且通过使用其他类型的作用域单元甚至可以实现维护起来更加优秀.简洁的代码,比如块作用域.随着ES6的推广,块作用域也将用得越来越广泛.本文是深入理解javascript作用域系列第四篇——块作用域 let for (var i= 0; i<10; i++) { console.log(i); } 上面这段是很熟