构建自己的AngularJS - 作用域和Digest(三)
2016-04-04 11:38
711 查看
作用域
第一章 作用域和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; }) } return scope.aValue; }, 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 ++; }); } return scope.aValue; }, 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”,在一个应用函数触发的时候应该是“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循环当前是哪种状态,函数都会立即返回而不是异步计表达式。
扫一扫,更多好文早知道
相关文章推荐
- AngularJS路由实例(uiRoute、ngRoute)
- angular.forEach在调接口中的应用
- AngularJs Scrope
- AngularJs Controllers
- AngularJS ng-model directive
- angularJs小应用----计算购物金额-动态改变邮费
- 初识angularJs
- Angular 根据 service 的状态更新 directive
- AngularJs 60分钟入门基础教程
- AngularJs 基础(60分钟入门) (转)
- 初识angularJs
- Angularjs学习笔记1_基本技巧
- AngularJs 60分钟入门基础教程
- Angular 根据 service 的状态更新 directive
- angularJs directives
- angularjs介绍
- AngularJS专题——路由
- Angularjs动态绑定HTML文本
- 详说Angular之指令(directive)
- angularjs