$eval
在 angularjs 中有几种方法去在 scope 的上下文中执行一个函数,最简单的就是 $eval 方法,它接受一个函数作为参数并且返回该函数返回的,参数函数接受 scope 作为参数。
Scope.prototype.$eval = function(expr, locals) {const self = this;return expr(self, locals);};
$eval 是让一部分函数可以通过参数直接访问到 scope 实例。
$apply
Scope.prototype.$apply = function(expr) {try {return this.$eval(expr);} finally {this.$digest();}};
$apply 使用 $eval 来执行一个函数,并且开启一次 digest 循环,$apply 意义在于有一些外部代码的执行没有通知 angularjs 但是也有可能改变 scope 上的值,所以我们用 scope 包裹着这样 angularjs 始终会抓取到 scope 上的变化。这就是所谓的将外部代码融入到 angularjs 的内部循环中。
$evalAsync
我们经常需要延迟执行一段代码,最简单的方法就是使用 setTimeout 来执行一个回调,在 angularjs 中使用 $timeout 来解决,用 $apply 来将延迟函数合并到 digest 循环中,但是我们希望只是延迟到同步 digest 队列末端而不是开启异步任务,中间说不定又隔了许多操作。
describe('$evalAsync', function() {let scope;beforeEach(function() {scope = new Scope();});it('executes given 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) {scope.asyncEvaluated = true;});scope.asyncEvaluatedImmediately = scope.asyncEvaluated;});scope.$digest();expect(scope.asyncEvaluated).to.equals(true);expect(scope.asyncEvaluatedImmediately).to.equals(false);});});
Scope.prototype.$evalAsync = function(expr) {this.$$asyncQueue.push({scope: this, expression: expr});};Scope.prototype.$digest = function () {let ttl = 10;let dirty;this.$$lastDirtyWatch = null;do {while (this.$$asyncQueue.length) {let asyncTask = this.$$asyncQueue.shift();asyncTask.scope.$eval(asyncTask.expression);}dirty = this.$$digestOnce();if (dirty && !(ttl--)) {throw ('10 digest iterations reached');}} while (dirty);}
$evalAsync 被调用时传入的 expr 不会立即被调用,这个和 $apply 和 $eval 有区别,只会推入一个数组,当第一次 digest 时 listenerFn 函数还没有执行 $$asyncQueue 长度还为空,expr 将会在第二轮的开始执行。这里很像浏览器的异步消息队列。同时传入 scope 用来提供改 scope 的 $eval 函数十分巧妙
清空数组在 digestOnce 调用之前使得 scope 可以捕获到变化
在 $watch 函数中执行 $evalAsync
通常来说 $watch 是不应该执行其他函数的,应该是无副作用的,但是为了防止用户可能的行为,我们应该允许。
it('executes $evalAsynced 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).to.equals(2);});
digest 会进行两次循环,但是第二次的时候由于 asyncEvaluatedTimes 仍小于 2 ,所以仍会 push 进数组,导致本轮 digest 漏了延迟执行的函数,所以此时 asyncEvaluatedTimes 为 1 ,我们需要加宽脏检查的条件兼容延迟队列。
Scope.prototype.$digest = function () {let ttl = 10;let dirty;this.$$lastDirtyWatch = null;do {while (this.$$asyncQueue.length) {let asyncTask = this.$$asyncQueue.shift();asyncTask.scope.$eval(asyncTask.expression);}dirty = this.$$digestOnce();if (dirty && !(ttl--)) {throw ('10 digest iterations reached');}} while (dirty || this.$$asyncQueue.length);}
由于第二轮开始就不脏了,dirty 为 false,所以不会走 dirty && !(ttl—) 逻辑,导致无限循环:
if ((dirty || this.$$asyncQueue.length) && !(ttl--)) {throw ('10 digest iterations reached');}
scope 状态
为了确保 $evalAsync 会让代码很快执行,我们需要确保 digest 在执行,而不是等其他改动触发了 digest,我们将 scope 设置为三种状态 $apply,$digest,null,我们创建两个函数来修改,清空它。
Scope.prototype.$beginPhase = function(phase) {if (this.$$phase) {throw this.$$phase + ' already in progress.'; //相信你一定见过}this.$$phase = phase;};Scope.prototype.$clearPhase = function() {this.$$phase = null;};
Scope.prototype.$apply = function(expr) {try {this.$beginPhase('$apply');return this.$eval(expr);} finally {this.$clearPhase();this.$digest();}};
Scope.prototype.$digest = function () {let ttl = 10;let dirty;this.$$lastDirtyWatch = null;this.$beginPhase('$digest');do {while (this.$$asyncQueue.length) {let asyncTask = this.$$asyncQueue.shift();asyncTask.scope.$eval(asyncTask.expression);}dirty = this.$$digestOnce();if ((dirty || this.$$asyncQueue.length) && !(ttl--)) {this.$clearPhase();throw ('10 digest iterations reached');}} while (dirty || this.$$asyncQueue.length);this.$clearPhase();}
scope 状态防止的是在 watchFn 和 listenerFn 以及 $apply 函数中调用了/触发了 $digest 导致当前 digest 中止开启新 digest 。
Scope.prototype.$evalAsync = function(expr) {const self = this;if (!self.$$phase && !self.$$asyncQueue.length) {setTimeout(function() {if (self.$$asyncQueue.length) {self.$digest();}}, 0);}self.$$asyncQueue.push({scope: self, expression: expr});};
在 $evalAsync 中加入判断,这样它在 watchFn 和 listenerFn 以及 $apply 外调用时也能触发 digest。第一次 $evalAsync 会 push 进 $$asyncQueue 数组,使它长度不为空,使第二次直接 push ,等到执行异步代码时已经完成所有的同步代码,已经完成所有的 push 操作,此时会执行异步的 digest 操作。
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) {});// 在 watchFn 和 listenerFn 以及 $apply 外调用expect(scope.counter).to.equals(0);setTimeout(function() {expect(scope.counter).to.equals(1);done();}, 50);});
$applyAsync
和 $apply 函数一样是为了将外部代码融入 digest 循环,但不是立即执行,而是延迟执行,刚开始是为了处理 http 请求,当 http 响应时 digest 就会执行,这意味着如果有多个 http 就会有多个有多个 digest 性能非常差,angularjs 中使用 $http 内部就是使用了 $applyAsync 来实现的,彼此非常接近的HTTP响应将被合并成一个 digest 。
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).to.equals(1);scope.$applyAsync(function (scope) {scope.aValue = 'abc';});expect(scope.counter).to.equals(1);setTimeout(function () {expect(scope.counter).to.equals(2);done();}, 50);});
目前为止和 $evalAsync 一致,区别在于如果在 listenerFn 中调用 $evalAsync 仍会在同一轮 digest 中被调用,但是 $applyAsync 会下一轮 digest 调用,
it('never executes $applyAsynced 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).to.equals(false);setTimeout(function() {expect(scope.asyncApplied).to.equals(true);done();}, 50);});
Scope.prototype.$applyAsync = function(expr) {const 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);}};
我们只想 digest 一次,只想要一个计时器,再看一种情况,$applyAsync 之后立即调用 digest 应该把 $applyAsync 中的函数执行了,并且进行一次 digest
Scope.prototype.$digest = function () {let ttl = 10;let dirty;this.$$lastDirtyWatch = null;this.$beginPhase('$digest');if (this.$$applyAsyncId) {clearTimeout(this.$$applyAsyncId);this.$$flushApplyAsync(); //提取出来}do {while (this.$$asyncQueue.length) {let asyncTask = this.$$asyncQueue.shift();asyncTask.scope.$eval(asyncTask.expression);}dirty = this.$$digestOnce();if ((dirty || this.$$asyncQueue.length) && !(ttl--)) {this.$clearPhase();throw ('10 digest iterations reached');}} while (dirty || this.$$asyncQueue.length);this.$clearPhase();}
- 如果 $applyAsync 在 $digest 之前执行,那么它就会在下一轮 digest 执行
- 如果在 $digest 之后调用就会在下一轮 digest 执行
- $evalAsync 的逻辑是尽可能地在本轮 digest 中清空 $$asyncQueue 数组,如果在 digest 外调用 $evalAsync 那么就会开启一轮 digest
- $applyAsync 是尽量开启一轮新的 $digest ,如果恰好在 $digest 前调用 $applyAsync 那么就顺势融入这轮循环。
- $$asyncQueue 和 $$postDigestQueue 数组中的函数会在 $digest 函数中执行,而 $$applyAsyncQueue 中的函数会在 $applyAsync 中的 setTimeout 中执行,执行完调用 $digest。
为什么 $applyAsync 函数不需要加宽条件因为它在 $digest 函数调用之前(整个 digest 开启之前)就执行清空数组,如果在 digest 过程中调用则会等到下一轮清空。
$$postDigest
$$postDigest 为了将代码延迟到下一轮 digest 完成,它的代码只会执行一次,而且不会引起一轮 digest,所以在这里修改 scope 上的东西不会触发脏检查,可以手动 $apply 或者 $digest ,$digest 后加入下面代码即可
it('does 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).to.equals('original value');scope.$digest();expect(scope.watchedValue).to.equals('changed value');});
while (this.$$postDigestQueue.length) {this.$$postDigestQueue.shift()();}
错误处理
由于 $evalAsync 和 $$postDigest 都在 $digest 内执行 shift 只要把 shift 用 try catch 包起来即可,$applyAsync 只要把 $$flushApplyAsync 包起来即可。
$watchGroup
接受 watchFn 函数数组,监听的数据变化时返回一个监听数据的数组
it('take watches as an array and calls listener with arrays ', function () {let gotNewValues, gotOldValues;scope.aValue = 1;scope.anotherValue = 2;scope.$watchGroup([function (scope) {return scope.aValue;},function (scope) {return scope.anotherValue}], function (newValues, oldValues, scope) {gotNewValues = newValues;gotOldValues = oldValues;});scope.$digest();expect(gotNewValues).to.equals([1, 2]);expect(gotOldValues).to.equals([1, 2]);});
Scope.prototype.$watchGroup = function(watchFns, listenerFn) {const self = this;const newValues = new Array(watchFns.length);const oldValues = new Array(watchFns.length);_.forEach(watchFns, function(watchFn, i) {self.$watch(watchFn, function(newValue, oldValue) {newValues[i] = newValue;oldValues[i] = oldValue;listenerFn(newValues, oldValues, self);});});};
现在的问题在于整体的 listenerFn 被调用的太频繁,应该等到多个 watchFn 被确认是才会进行调用
Scope.prototype.$watchGroup = function(watchFns, listenerFn) {const self = this;const newValues = new Array(watchFns.length);const oldValues = new Array(watchFns.length);let changeReactionScheduled = false;function watchGroupListener() {listenerFn(newValues, oldValues, self);changeReactionScheduled = false;}_.forEach(watchFns, function(watchFn, i) {self.$watch(watchFn, function(newValue, oldValue) {newValues[i] = newValue;oldValues[i] = oldValue;if (!changeReactionScheduled) {changeReactionScheduled = true;self.$evalAsync(watchGroupListener);}});});};
只要几个 watch 监听的变量改变了,就会设置 changeReactionScheduled 为 true 将执行 listenerFn 一次延迟等遍历完之后执行。
Scope.prototype.$watchGroup = function(watchFns, listenerFn) {const self = this;const newValues = new Array(watchFns.length);const oldValues = new Array(watchFns.length);let changeReactionScheduled = false;let firstRun = true;if (watchFns.length === 0) {let shouldCall = true;self.$evalAsync(function() {if (shouldCall) {listenerFn(newValues, newValues, self);}});return function() {shouldCall = false;};}function watchGroupListener() {if (firstRun) {firstRun = false;listenerFn(newValues, newValues, self);} else {listenerFn(newValues, oldValues, self);}changeReactionScheduled = false;}let destroyFunctions = _.map(watchFns, function(watchFn, i) {return self.$watch(watchFn, function(newValue, oldValue) {newValues[i] = newValue;oldValues[i] = oldValue;if (!changeReactionScheduled) {changeReactionScheduled = true;self.$evalAsync(watchGroupListener);}});});return function() {_.forEach(destroyFunctions, function(destroyFunction) {destroyFunction();});};};
- $eval 和 $apply 将立即调用内部的函数,$apply 将开启调用 $digest,而 $eval 不会所以尽量不要在 digest 外使用,$apply 可以在 digest 使用做到了将外部代码合并到 digest 循环中。
- $evalAsync, $applyAsync, 和 $$postDigest 延迟调用代码,每一个都对应一个数组(消息队列),其中$$postDigest 最为特殊因为它触发在 digest 之后导致它所触发 scope 上的改变需要手动去调用 $digest。
