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

作用域

第一章 作用域和Digest(三)

$eval - 在当前作用域的上下文中执行代码

Angular有多种方式让你在当前作用域的上下文中执行代码。最简单的是$eval。传入一个函数当做其参数,然后将当前的作用域作为参数传给该函数,并执行它。然后它返回该函数的执行结果。$eval还有第二个可选的参数,它仅仅是被传递给将要执行的函数。

有几个单元测试展示了我们如何使用$eval

test/scope_spec.js

it("execute $eval‘ed function and return the result", function(){
    scope.aValue = 42;

    var result = scope.$eval(function(scope){
        return scope.aValue;
    })

    expect(result).toBe(42);
});

it("pass the second $eval argument straight through", function(){
    scope.aValue = 42;
    var result = scope.$eval(function(scope, arg){
        return scope.aValue + arg;
    }, 2);

    expect(result).toBe(44);
});

实现$eval非常简单:

src/scope.js

Scope.prototype.$eval = function(expr, locals){
    return expr(this, locals);
};

使用这种迂回的方式去触发一个函数有什么目的呢?有人认为:$eval仅仅能够让一些处理作用域的代码调用起来稍微清楚一点。我们即将看到,$eval$apply的基石。

然而,使用$eval最有趣的地方我们使用表达式代替原函数。和$watch一样,你可以给$eval一个字符串表达式。它会编译该表达式,在作用域的上下文中执行。我们会在该书的第二部分实现。

$apply - 整合使用Digest循环的外部代码

可能作用域中最广为人知的函数就是$apply了。他被认为是外部类库集成到Angular中最标准的方法。原因是:

$apply使用函数作为其参数。他使用$eval执行该函数,然后启动通过触发$digest启动digest循环。有几个测试案例如下:

test/scope_spec.js

it("execute $apply‘ed function and starts the digest", function(){
    scope.aValue = ‘someValue‘;
    scope.counter = 0;

    scope.$watch(
        function(scope){ return scope.aValue; },
        function(newValue, oldValue, scope){
            scope.counter ++;
        });

    scope.$digest();
    expect(scope.counter).toBe(1);

    scope.$apply(function(scope){
        scope.aValue = ‘someOtherValue‘;
    });

    expect(scope.counter).toBe(2);
});

我们有一个监控函数监控scope.aValue,并且让计数器自增。当$apply触发的时候我们测试该监控函数时候被执行。

下面是让该测试案例通过的一个简单的实现:

src/scope.js

Scope.prototype.$apply = function(expr){
    try{
        return this.$eval(expr);
    }finally{
        this.$digest();
    }
};

$digest在finally块中被调用,来确保即使提供的函数抛出异常digest循环一定会发生。

$apply的最大思想是我们可以执行一些Angular没有意识到的代码。而这些代码可能改变作用域上的内容,只要我们用$apply包装了这些代码,我们

可以确保该作用域上的任一个监控都能接受到这些变化。当人们谈论在“Angular生命循环”中使用$apply集成代码时,这基本上就是他们要表达的意思。

$evalAsync - 推迟执行

在Javascript中,推迟执行一块代码很平常 - 推迟其执行到未来的某个时间点,直到当前的执行完成。通常是通过调用setTimeout()传入0(或者一个非常小)的延迟的参数来实现的。

这种模式也可以应用到Angular应用上,尽管比较推荐的方法是使用$timeout服务,其中,在digest中使用$apply集成延迟函数

但是在Angular中有另外一种方法推迟代码的执行,那就是Scope中的$evalAsync函数。$evalAsync提供一个函数作为参数,推迟调度其执行,但是仍在其当前正在运行的digest中。例如,你可以,在一个监听函数中退职一个代码的执行,了解到虽然这些代码被推迟了,但是在当前的digest遍历中仍然会被触发。

相比于$timeout$evalAsync更可取的原因和浏览器的事件循环有关。当你使用$timeout去调度你的工作,你把你的控制权给了浏览器,让浏览器决定什么时候去运行你的调度。在你的工作到达限制时间之前,浏览器可能选择其他工作先去执行。例如,渲染页面,运行点击事件,或者处理Ajax响应。与之不同的是,$evalAsync在执行工作方面更加的严格。因为他在当前正在运行的digest中执行,可以保证在其一定在浏览器运行其他事情之前运行。$timeout$evalAsync的区别在你想要阻止不必要的渲染的时候更加明显:为什么要让浏览器渲染即将被覆盖掉的DOM变化呢?

下面是关于$evalAsync的单元测试:

test/scope_spec.js

it("execute $evalAsync‘ed function later in the same cycle", function(){
    scope.aValue = [1, 2, 3];
    scope.asyncEvaluated = false;
    scope.asyncEvaluatedImmediately = false;

    scope.$watch(
        function(scope) { return scope.aValue; },
        function(newValue, oldValue, scope){
            scope.$evalAsync(function(){
                scope.asyncEvaluated = true;
            });

            scope.asyncEvaluatedImmediately = scope.asyncEvaluated;
        });

    scope.$digest();
    expect(scope.asyncEvaluated).toBe(true);
    expect(scope.asyncEvaluatedImmediately).toBe(false);
});

我们在监听函数中调用了$evalAsync,然后检查该函数时候在同一个digest中最后被执行。首先,我们需要去存储被调度的$evalAsync任务。

我们想要使用数组来存储,在Scope的构造函数中初始化:

src/scope.js

function Scope(){
    this.$$watchers = [];
	this.$$lastDirtyWatch = null;
    this.$$asyncQueue = [];
}

然后我们来定义$evalAsync,让其将要执行的函数加入该队列中:

src/scope.js

Scope.prototype.$evalAsync = function(expr){
    this.$$asyncQueue.push({scope: this, expression: expr});
};

我们要明确的在当前队列的对象中存储当前的作用域是和作用域的继承有关的,我们将在下一章讨论这个问题。

对于将要被执行的函数,我们先将他们记录下来,事实上,我们还需要去执行他们。那将是在$digest中发生:首先在$digest中我们要消耗该队列中的所有内容,然后通过使用$eval来触发所有被延迟的函数:

src/scope.js

Scope.prototype.$digest = function(){
    var tt1 = 10;
    var dirty;
    this.$$lastDirtyWatch = null;
	do {
		while (this.$$asyncQueue.length){
			var asyncTask = this.$$asyncQueue.shift();
			asyncTask.scope.$eval(asyncTask.expression);
		}
		dirty = this.$$digestOnce();
        if(dirty && !(tt1 --)) {
            throw ‘10 digest iterations reached‘;
        }
    } while (dirty);
};

该实现保证了当scope是脏的情况下推迟函数的执行,之后将会触发函数,但是仍然能在同一个digest中。这满足了我们的单元测试。

在监控函数中使用$evalAsync

在上一节中,我们看到了在监听函数中使用$evalAsync调度函数仍在同一个digest循环中延迟执行。但是如果你在监控函数中使用$evalAsync会发生什么呢?假定有一件事情你不需要去做,因为监控函数应该是没有副作用的。但是他仍然可能去做,所以我们应该确定这不能在digest中造成破坏。

我们思考一个场景,在监控函数中使用一次$evalAsync,每件事看起来都是有序的。在我们当前的视线中,下面的测试案例应该都能通过:

test/scope_spec.js

it("executes $evalAsync‘ed functions added by watch functions", function(){
    scope.aValue = [1, 2, 3];
    scope.asyncEvaluated = false;

    scope.$watch(
        function(scope){
            if(!scope.asyncEvaluated){
                scope.$evalAsync(function(){
                    scope.asyncEvaluated = true;
                })
            }
        },
        function(newValue, oldValue, scope) {} );

    scope.$digest();
    expect(scope.asyncEvaluated).toBe(true);
});

那么问题是什么呢?正如我们看到的,我们在最后一个监控是脏的情况下保持digest循环继续。在上面测试案例中,这种情况就发生在第一次遍历中,当我们在监控函数中返回scope.aValue。这引起了digest进入下一次遍历,在这次遍历中,他调用了我们使用$evalAsync调度的函数。但是当没有监控是脏的情况下,我们调度$evalAsync呢?

test/scope_spec.js

it("executes $evalAsync‘ed functions even when not dirty", function(){
    scope.aValue = [1, 2, 3];
    scope.asyncEvaluatedTimes = 0;

    scope.$watch(
        function(scope){
            if(scope.asyncEvaluatedTimes < 2){
                scope.$evalAsync(function(scope){
                    scope.asyncEvaluatedTimes ++;
                });
            }
        },
        function(newValue, oldValue, scope) {});

    scope.$digest();
    expect(scope.asyncEvaluatedTimes).toBe(2);

});

这个版本做了两次$evalAsync。在第二次中,监控函数不是脏的,因为scope.aValue没有发生变化。这意味着$evalAsync没有运行,因为$digest已经停止了。尽管他会在下一次digest中运行,但是我们希望他这次中运行。同时意味着我们需要调整$digest的结束条件,查看在异步队列中是否有内容需要去运行:

src/scope.js

Scope.prototype.$digest = function(){
    var tt1 = 10;
    var dirty;
    this.$$lastDirtyWatch = null;
	do {
		while (this.$$asyncQueue.length){
			var asyncTask = this.$$asyncQueue.shift();
			asyncTask.scope.$eval(asyncTask.expression);
		}
		dirty = this.$$digestOnce();
		if(dirty && !(tt1 --)) {
			throw ‘10 digest iterations reached‘;
		}
	} while (dirty || this.$$asyncQueue.length);
};

测试案例通过了,但是现在我们引入了一个问题。如果一个监控函数一直使用$evalAsync去调度一些事情呢?我们可能希望引起循环达到最大值,实际上,并没有:

test/scope_spec.js

it("eventually halts $evalAsyncs added by watches", function(){
    scope.aValue = [1, 2, 3];

    scope.$watch(
        function(scope){
            scope.$evalAsync(function(scope){});
            return scope.aValue;
        },
        function(newValue, oldValue, scope) {}
    );

    expect(function(){ scope.$digest()}).toThrow();
});

该测试会一直执行下去,因为$digest中的循环一直不会结束。我们需要做的是在TTL检查中也检查异步队列的状态:

src/scope.js

Scope.prototype.$digest = function(){
    var tt1 = 10;
    var dirty;
    this.$$lastDirtyWatch = null;
	do {
		while (this.$$asyncQueue.length){
			var asyncTask = this.$$asyncQueue.shift();
			asyncTask.scope.$eval(asyncTask.expression);
		}
		dirty = this.$$digestOnce();
		if((dirty || this.$$asyncQueue.length) && !(tt1 --)) {
			throw ‘10 digest iterations reached‘;
		}
	} while (dirty || this.$$asyncQueue.length);
};

不论digest因为是脏的情况下运行,还是因为在队列中有内容的情况下运行,现在我们都可以确定其会结束。

作用域相位

$evalAsync另一个功能是调度一个没有准备运行的$digest去运行。也就是,不论当你什么时候调用$evalAsync,你都可以确定你正在推迟的函数很快会被触发,而不是等到有些内容去触发一个digest之后。

尽管$evalAsync不会调度一个$digest,比较好的方式是使用$applyAsync去异步执行一个有digest的代码,让我们开始下个章节吧。

这里为了这种情况可以工作,对于$evalAsync来说需要有方法去检查$digest是否正在运行,因为那种情况下,他不能够去打扰一个正在执行的代码。因为这个原因,Angular作用域实现了名叫相位(phase)属性,是作用域上的一个简单字符型的属性,存储着当前正在运行的一些信息。

作为一个单元测试,让我们给这个名叫$$phase的字段设置一个期望,在digest过程中,值应该是“digest”,在一个应用函数触发的时候应该是“apply”,其他情况为null:

test/scope_spec.js

it("has a $$phase field whose value is the current digest phase", function(){
    scope.aValue = [1, 2, 3];
    scope.phaseInWatchFunction = undefined;
    scope.phaseInListenerFunction = undefined;
    scope.phaseInApplyFunction = undefined;

    scope.$watch(
        function(scope){
            scope.phaseInWatchFunction = scope.$$phase;
		},
		function(newValue, oldValue, scope){
			scope.phaseInListenerFunction = scope.$$phase;
        }
    );

    scope.$apply(function(scope){
        scope.phaseInApplyFunction = scope.$$phase;
    });

    expect(scope.phaseInWatchFunction).toBe("$digest");
    expect(scope.phaseInListenerFunction).toBe("$digest");
    expect(scope.phaseInApplyFunction).toBe("$apply");
});

在此我们不需要显示的调用$digest,因为$apply已经帮我们做了。

在Scope的构造函数中,让我们加入$$phase字段,并初始化为null;

src/scope.js

function Scope(){
    this.$$watchers = [];
	this.$$lastDirtyWatch = null;
    this.$$asyncQueue = [];
	this.$$phase = null;
}

下面,让我们定义一组函数用来控制相位:一个用来设置他,一个用来清除他。同时我们在设置函数添加一个校验,来确保当其正在活动时,我们没有试图去设置他。

src/scope.js

Scope.prototype.$beginPhase = function(phase){
    if(this.$$phase){
		throw this.$$phase + " already in progress.";
	}
	this.$$phase = phase;
};

Scope.prototype.$clearPhase = function(){
    this.$$phase = null;
};

$digest中,我们在digest循环外设置phase为”$digest”:

src/scope.js

Scope.prototype.$digest = function(){
    var tt1 = 10;
    var dirty;
    this.$$lastDirtyWatch = null;
	this.$beginPhase("$digest");
	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();
};

让我们也来改变$apply让其能够为他自己设置该相位:

src/scope.js

Scope.prototype.$apply = function(expr){
    try{
        this.$beginPhase("$apply");
        return this.$eval(expr);
    }finally{
        this.$clearPhase();
        this.$digest();
    }
};

最后,我们可以将通过$evalAsync来调用$digest。让我们先为这个需求定义一个单元测试:

test/scope_spec.js

it("schedules a digest 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) {} );

    expect(scope.counter).toBe(0);
    setTimeout(function() {
        expect(scope.counter).toBe(1);
        done();
    }, 50);

});

我们检测digest确实运行了,不是在$evalAsync调用之后,而是稍微在其后面。我们定义“稍微后面”是指50毫秒后。为了让setTimeout能够咋Jasmine中运行,我们使用了异步测试支持:该测试案例接受一个额外的参数作为回调参数,一旦我们调用它,他会完成整个测试,我们已经在延迟之后这样做了。

现在$evalAsync可以测试作用域的当前相位了,如果没有(还没有异步的任务被调度),调度digest运行。

src/scope.js

Scope.prototype.$evalAsync = function(expr){
    var self = this;
    if(!self.$$phase && !self.asyncQueue.length){
		setTimeout(function(){
			if(self.$$asyncQueue.length){
				self.$digest();
			}
		}, 0);
	}
	this.$$asyncQueue.push({scope: this, expression: expr});
};

通过该实现,你可以确定当你调用$evalAsync时,digest会在不久之后运行,不论你什么时间、什么时候调用它。

当你在digest运行过程中调用$evalAsync,你的函数会在这次digest中被计算。如果没有digest正在运行,一个digest会被启动。我们使用setTimeout在digest之前做一个稍微的延迟。$evalAsync的这种调用方式可以确保:不论digest循环当前是那种状态,函数都会立即返回而不是异步计表达式。

扫一扫,更多好文早知道:

时间: 2024-11-09 14:08:37

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

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

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

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

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

作用域 第一章 作用域和Digest(四) 联合$apply调用 - $applyAsync 不论在digest里面还是外面调用$evalAsync去延迟工作,他实际是为之前的使用案例设计的.之所以在setTimeout中调用digest是为了在digest循环外面调用$evalAsync时防止混淆. 针对在digest循环外部异步调用$apply的情况,同样有一个名为$applyAsync来处理.其使用类似于$apply - 为了集成没有意识到Angular digest循环的代码.和$app

构建自己的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 简介 AngularJS 是由 Google 发起的一款开源的前端 MVC 脚本框架,既适合做普通 WEB 应用也可以做 SPA(单页面应用,所有的用户操作都在一个页面中完成).与同为 MVC 框架的 Dojo 的定位不同,AngularJS 在功能上更加轻量,而相比于 jQuery,AngularJS 又帮您省去了许多机械的绑定工作.在一些对开发速度要求高,功能模块不需要太丰富的非企业级 WEB 应用上,AngularJS 是一个非常好的选择.AngularJS 最为复杂同

angularJS 作用域

<!doctype html><html ng-app="firstApp"><head> <meta charset="utf-8"> <script src="angular-1.3.0.js"></script></head><body><pre> </pre><div ng-controller="par

深入理解javascript作用域系列第三篇

前面的话 一般认为,javascript代码在执行时是由上到下一行一行执行的.但实际上这并不完全正确,主要是因为声明提升的存在.本文是深入理解javascript作用域系列第三篇--声明提升(hoisting) 变量声明提升 a = 2 ; var a; console.log( a ); 直觉上,会认为是undefined,因为var a声明在a = 2;之后,可能变量被重新赋值了,因为会被赋予默认值undefined.但是,真正的输出结果是2 console.log( a ) ; var a