$eval

在 angularjs 中有几种方法去在 scope 的上下文中执行一个函数,最简单的就是 $eval 方法,它接受一个函数作为参数并且返回该函数返回的,参数函数接受 scope 作为参数。

  1. Scope.prototype.$eval = function(expr, locals) {
  2. const self = this;
  3. return expr(self, locals);
  4. };

$eval 是让一部分函数可以通过参数直接访问到 scope 实例。

$apply

  1. Scope.prototype.$apply = function(expr) {
  2. try {
  3. return this.$eval(expr);
  4. } finally {
  5. this.$digest();
  6. }
  7. };

$apply 使用 $eval 来执行一个函数,并且开启一次 digest 循环,$apply 意义在于有一些外部代码的执行没有通知 angularjs 但是也有可能改变 scope 上的值,所以我们用 scope 包裹着这样 angularjs 始终会抓取到 scope 上的变化。这就是所谓的将外部代码融入到 angularjs 的内部循环中。

$evalAsync

我们经常需要延迟执行一段代码,最简单的方法就是使用 setTimeout 来执行一个回调,在 angularjs 中使用 $timeout 来解决,用 $apply 来将延迟函数合并到 digest 循环中,但是我们希望只是延迟到同步 digest 队列末端而不是开启异步任务,中间说不定又隔了许多操作。

  1. describe('$evalAsync', function() {
  2. let scope;
  3. beforeEach(function() {
  4. scope = new Scope();
  5. });
  6. it('executes given function later in the same cycle', function() {
  7. scope.aValue = [1, 2, 3];
  8. scope.asyncEvaluated = false;
  9. scope.asyncEvaluatedImmediately = false;
  10. scope.$watch(
  11. function(scope) { return scope.aValue; },
  12. function(newValue, oldValue, scope) {
  13. scope.$evalAsync(function(scope) {
  14. scope.asyncEvaluated = true;
  15. });
  16. scope.asyncEvaluatedImmediately = scope.asyncEvaluated;
  17. }
  18. );
  19. scope.$digest();
  20. expect(scope.asyncEvaluated).to.equals(true);
  21. expect(scope.asyncEvaluatedImmediately).to.equals(false);
  22. });
  23. });
  1. Scope.prototype.$evalAsync = function(expr) {
  2. this.$$asyncQueue.push({scope: this, expression: expr});
  3. };
  4. Scope.prototype.$digest = function () {
  5. let ttl = 10;
  6. let dirty;
  7. this.$$lastDirtyWatch = null;
  8. do {
  9. while (this.$$asyncQueue.length) {
  10. let asyncTask = this.$$asyncQueue.shift();
  11. asyncTask.scope.$eval(asyncTask.expression);
  12. }
  13. dirty = this.$$digestOnce();
  14. if (dirty && !(ttl--)) {
  15. throw ('10 digest iterations reached');
  16. }
  17. } while (dirty);
  18. }

$evalAsync 被调用时传入的 expr 不会立即被调用,这个和 $apply 和 $eval 有区别,只会推入一个数组,当第一次 digest 时 listenerFn 函数还没有执行 $$asyncQueue 长度还为空,expr 将会在第二轮的开始执行。这里很像浏览器的异步消息队列。同时传入 scope 用来提供改 scope 的 $eval 函数十分巧妙

清空数组在 digestOnce 调用之前使得 scope 可以捕获到变化

在 $watch 函数中执行 $evalAsync

通常来说 $watch 是不应该执行其他函数的,应该是无副作用的,但是为了防止用户可能的行为,我们应该允许。

  1. it('executes $evalAsynced functions even when not dirty', function () {
  2. scope.aValue = [1, 2, 3];
  3. scope.asyncEvaluatedTimes = 0;
  4. scope.$watch(
  5. function (scope) {
  6. if (scope.asyncEvaluatedTimes < 2) {
  7. scope.$evalAsync(function (scope) {
  8. scope.asyncEvaluatedTimes++;
  9. });
  10. }
  11. return scope.aValue;
  12. },
  13. function (newValue, oldValue, scope) {
  14. }
  15. );
  16. scope.$digest();
  17. expect(scope.asyncEvaluatedTimes).to.equals(2);
  18. });

digest 会进行两次循环,但是第二次的时候由于 asyncEvaluatedTimes 仍小于 2 ,所以仍会 push 进数组,导致本轮 digest 漏了延迟执行的函数,所以此时 asyncEvaluatedTimes 为 1 ,我们需要加宽脏检查的条件兼容延迟队列。

  1. Scope.prototype.$digest = function () {
  2. let ttl = 10;
  3. let dirty;
  4. this.$$lastDirtyWatch = null;
  5. do {
  6. while (this.$$asyncQueue.length) {
  7. let asyncTask = this.$$asyncQueue.shift();
  8. asyncTask.scope.$eval(asyncTask.expression);
  9. }
  10. dirty = this.$$digestOnce();
  11. if (dirty && !(ttl--)) {
  12. throw ('10 digest iterations reached');
  13. }
  14. } while (dirty || this.$$asyncQueue.length);
  15. }

由于第二轮开始就不脏了,dirty 为 false,所以不会走 dirty && !(ttl—) 逻辑,导致无限循环:

  1. if ((dirty || this.$$asyncQueue.length) && !(ttl--)) {
  2. throw ('10 digest iterations reached');
  3. }

scope 状态

为了确保 $evalAsync 会让代码很快执行,我们需要确保 digest 在执行,而不是等其他改动触发了 digest,我们将 scope 设置为三种状态 $apply,$digest,null,我们创建两个函数来修改,清空它。

  1. Scope.prototype.$beginPhase = function(phase) {
  2. if (this.$$phase) {
  3. throw this.$$phase + ' already in progress.'; //相信你一定见过
  4. }
  5. this.$$phase = phase;
  6. };
  7. Scope.prototype.$clearPhase = function() {
  8. this.$$phase = null;
  9. };
  1. Scope.prototype.$apply = function(expr) {
  2. try {
  3. this.$beginPhase('$apply');
  4. return this.$eval(expr);
  5. } finally {
  6. this.$clearPhase();
  7. this.$digest();
  8. }
  9. };
  1. Scope.prototype.$digest = function () {
  2. let ttl = 10;
  3. let dirty;
  4. this.$$lastDirtyWatch = null;
  5. this.$beginPhase('$digest');
  6. do {
  7. while (this.$$asyncQueue.length) {
  8. let asyncTask = this.$$asyncQueue.shift();
  9. asyncTask.scope.$eval(asyncTask.expression);
  10. }
  11. dirty = this.$$digestOnce();
  12. if ((dirty || this.$$asyncQueue.length) && !(ttl--)) {
  13. this.$clearPhase();
  14. throw ('10 digest iterations reached');
  15. }
  16. } while (dirty || this.$$asyncQueue.length);
  17. this.$clearPhase();
  18. }

scope 状态防止的是在 watchFn 和 listenerFn 以及 $apply 函数中调用了/触发了 $digest 导致当前 digest 中止开启新 digest 。

  1. Scope.prototype.$evalAsync = function(expr) {
  2. const self = this;
  3. if (!self.$$phase && !self.$$asyncQueue.length) {
  4. setTimeout(function() {
  5. if (self.$$asyncQueue.length) {
  6. self.$digest();
  7. }
  8. }, 0);
  9. }
  10. self.$$asyncQueue.push({scope: self, expression: expr});
  11. };

在 $evalAsync 中加入判断,这样它在 watchFn 和 listenerFn 以及 $apply 外调用时也能触发 digest。第一次 $evalAsync 会 push 进 $$asyncQueue 数组,使它长度不为空,使第二次直接 push ,等到执行异步代码时已经完成所有的同步代码,已经完成所有的 push 操作,此时会执行异步的 digest 操作。

  1. it('schedules a digest in $evalAsync', function(done) {
  2. scope.aValue = 'abc';
  3. scope.counter = 0;
  4. scope.$watch(
  5. function(scope) { return scope.aValue; },
  6. function(newValue, oldValue, scope) {
  7. scope.counter++;
  8. }
  9. );
  10. scope.$evalAsync(function(scope) {
  11. });
  12. // 在 watchFn 和 listenerFn 以及 $apply 外调用
  13. expect(scope.counter).to.equals(0);
  14. setTimeout(function() {
  15. expect(scope.counter).to.equals(1);
  16. done();
  17. }, 50);
  18. });

$applyAsync

和 $apply 函数一样是为了将外部代码融入 digest 循环,但不是立即执行,而是延迟执行,刚开始是为了处理 http 请求,当 http 响应时 digest 就会执行,这意味着如果有多个 http 就会有多个有多个 digest 性能非常差,angularjs 中使用 $http 内部就是使用了 $applyAsync 来实现的,彼此非常接近的HTTP响应将被合并成一个 digest 。

  1. it('allows async $apply with $applyAsync', function (done) {
  2. scope.counter = 0;
  3. scope.$watch(
  4. function (scope) {
  5. return scope.aValue;
  6. },
  7. function (newValue, oldValue, scope) {
  8. scope.counter++;
  9. }
  10. );
  11. scope.$digest();
  12. expect(scope.counter).to.equals(1);
  13. scope.$applyAsync(function (scope) {
  14. scope.aValue = 'abc';
  15. });
  16. expect(scope.counter).to.equals(1);
  17. setTimeout(function () {
  18. expect(scope.counter).to.equals(2);
  19. done();
  20. }, 50);
  21. });

目前为止和 $evalAsync 一致,区别在于如果在 listenerFn 中调用 $evalAsync 仍会在同一轮 digest 中被调用,但是 $applyAsync 会下一轮 digest 调用,

  1. it('never executes $applyAsynced function in the same cycle', function(done) {
  2. scope.aValue = [1, 2, 3];
  3. scope.asyncApplied = false;
  4. scope.$watch(
  5. function(scope) { return scope.aValue; },
  6. function(newValue, oldValue, scope) {
  7. scope.$applyAsync(function(scope) {
  8. scope.asyncApplied = true;
  9. });
  10. }
  11. );
  12. scope.$digest();
  13. expect(scope.asyncApplied).to.equals(false);
  14. setTimeout(function() {
  15. expect(scope.asyncApplied).to.equals(true);
  16. done();
  17. }, 50);
  18. });
  1. Scope.prototype.$applyAsync = function(expr) {
  2. const self = this;
  3. self.$$applyAsyncQueue.push(function() {
  4. self.$eval(expr);
  5. });
  6. if (self.$$applyAsyncId === null) {
  7. self.$$applyAsyncId = setTimeout(function() {
  8. self.$apply(function() {
  9. while (self.$$applyAsyncQueue.length) {
  10. self.$$applyAsyncQueue.shift()();
  11. }
  12. self.$$applyAsyncId = null;
  13. });
  14. }, 0);
  15. }
  16. };

我们只想 digest 一次,只想要一个计时器,再看一种情况,$applyAsync 之后立即调用 digest 应该把 $applyAsync 中的函数执行了,并且进行一次 digest

  1. Scope.prototype.$digest = function () {
  2. let ttl = 10;
  3. let dirty;
  4. this.$$lastDirtyWatch = null;
  5. this.$beginPhase('$digest');
  6. if (this.$$applyAsyncId) {
  7. clearTimeout(this.$$applyAsyncId);
  8. this.$$flushApplyAsync(); //提取出来
  9. }
  10. do {
  11. while (this.$$asyncQueue.length) {
  12. let asyncTask = this.$$asyncQueue.shift();
  13. asyncTask.scope.$eval(asyncTask.expression);
  14. }
  15. dirty = this.$$digestOnce();
  16. if ((dirty || this.$$asyncQueue.length) && !(ttl--)) {
  17. this.$clearPhase();
  18. throw ('10 digest iterations reached');
  19. }
  20. } while (dirty || this.$$asyncQueue.length);
  21. this.$clearPhase();
  22. }
  1. 如果 $applyAsync 在 $digest 之前执行,那么它就会在下一轮 digest 执行
  2. 如果在 $digest 之后调用就会在下一轮 digest 执行
  3. $evalAsync 的逻辑是尽可能地在本轮 digest 中清空 $$asyncQueue 数组,如果在 digest 外调用 $evalAsync 那么就会开启一轮 digest
  4. $applyAsync 是尽量开启一轮新的 $digest ,如果恰好在 $digest 前调用 $applyAsync 那么就顺势融入这轮循环。
  5. $$asyncQueue 和 $$postDigestQueue 数组中的函数会在 $digest 函数中执行,而 $$applyAsyncQueue 中的函数会在 $applyAsync 中的 setTimeout 中执行,执行完调用 $digest。
  6. 为什么 $applyAsync 函数不需要加宽条件因为它在 $digest 函数调用之前(整个 digest 开启之前)就执行清空数组,如果在 digest 过程中调用则会等到下一轮清空。

    $$postDigest

    $$postDigest 为了将代码延迟到下一轮 digest 完成,它的代码只会执行一次,而且不会引起一轮 digest,所以在这里修改 scope 上的东西不会触发脏检查,可以手动 $apply 或者 $digest ,$digest 后加入下面代码即可

    1. it('does not include $$postDigest in the digest', function() {
    2. scope.aValue = 'original value';
    3. scope.$$postDigest(function() {
    4. scope.aValue = 'changed value';
    5. });
    6. scope.$watch(
    7. function(scope) {
    8. return scope.aValue;
    9. },
    10. function(newValue, oldValue, scope) {
    11. scope.watchedValue = newValue;
    12. }
    13. );
    14. scope.$digest();
    15. expect(scope.watchedValue).to.equals('original value');
    16. scope.$digest();
    17. expect(scope.watchedValue).to.equals('changed value');
    18. });
    1. while (this.$$postDigestQueue.length) {
    2. this.$$postDigestQueue.shift()();
    3. }

    错误处理

    由于 $evalAsync 和 $$postDigest 都在 $digest 内执行 shift 只要把 shift 用 try catch 包起来即可,$applyAsync 只要把 $$flushApplyAsync 包起来即可。

    $watchGroup

    接受 watchFn 函数数组,监听的数据变化时返回一个监听数据的数组

    1. it('take watches as an array and calls listener with arrays ', function () {
    2. let gotNewValues, gotOldValues;
    3. scope.aValue = 1;
    4. scope.anotherValue = 2;
    5. scope.$watchGroup([
    6. function (scope) {
    7. return scope.aValue;
    8. },
    9. function (scope) {
    10. return scope.anotherValue
    11. }
    12. ], function (newValues, oldValues, scope) {
    13. gotNewValues = newValues;
    14. gotOldValues = oldValues;
    15. });
    16. scope.$digest();
    17. expect(gotNewValues).to.equals([1, 2]);
    18. expect(gotOldValues).to.equals([1, 2]);
    19. });
    1. Scope.prototype.$watchGroup = function(watchFns, listenerFn) {
    2. const self = this;
    3. const newValues = new Array(watchFns.length);
    4. const oldValues = new Array(watchFns.length);
    5. _.forEach(watchFns, function(watchFn, i) {
    6. self.$watch(watchFn, function(newValue, oldValue) {
    7. newValues[i] = newValue;
    8. oldValues[i] = oldValue;
    9. listenerFn(newValues, oldValues, self);
    10. });
    11. });
    12. };

    现在的问题在于整体的 listenerFn 被调用的太频繁,应该等到多个 watchFn 被确认是才会进行调用

    1. Scope.prototype.$watchGroup = function(watchFns, listenerFn) {
    2. const self = this;
    3. const newValues = new Array(watchFns.length);
    4. const oldValues = new Array(watchFns.length);
    5. let changeReactionScheduled = false;
    6. function watchGroupListener() {
    7. listenerFn(newValues, oldValues, self);
    8. changeReactionScheduled = false;
    9. }
    10. _.forEach(watchFns, function(watchFn, i) {
    11. self.$watch(watchFn, function(newValue, oldValue) {
    12. newValues[i] = newValue;
    13. oldValues[i] = oldValue;
    14. if (!changeReactionScheduled) {
    15. changeReactionScheduled = true;
    16. self.$evalAsync(watchGroupListener);
    17. }
    18. });
    19. });
    20. };

    只要几个 watch 监听的变量改变了,就会设置 changeReactionScheduled 为 true 将执行 listenerFn 一次延迟等遍历完之后执行。

    1. Scope.prototype.$watchGroup = function(watchFns, listenerFn) {
    2. const self = this;
    3. const newValues = new Array(watchFns.length);
    4. const oldValues = new Array(watchFns.length);
    5. let changeReactionScheduled = false;
    6. let firstRun = true;
    7. if (watchFns.length === 0) {
    8. let shouldCall = true;
    9. self.$evalAsync(function() {
    10. if (shouldCall) {
    11. listenerFn(newValues, newValues, self);
    12. }
    13. });
    14. return function() {
    15. shouldCall = false;
    16. };
    17. }
    18. function watchGroupListener() {
    19. if (firstRun) {
    20. firstRun = false;
    21. listenerFn(newValues, newValues, self);
    22. } else {
    23. listenerFn(newValues, oldValues, self);
    24. }
    25. changeReactionScheduled = false;
    26. }
    27. let destroyFunctions = _.map(watchFns, function(watchFn, i) {
    28. return self.$watch(watchFn, function(newValue, oldValue) {
    29. newValues[i] = newValue;
    30. oldValues[i] = oldValue;
    31. if (!changeReactionScheduled) {
    32. changeReactionScheduled = true;
    33. self.$evalAsync(watchGroupListener);
    34. }
    35. });
    36. });
    37. return function() {
    38. _.forEach(destroyFunctions, function(destroyFunction) {
    39. destroyFunction();
    40. });
    41. };
    42. };
  • 第一次运行 listenerFn 函数时新旧值做到引用相同
  • 只要有一个监听数据变化就将一次 listenerFn 延迟执行
  • 调用 $watch 时是不会产生副作用的,只是注册而已

    小结

  1. $eval 和 $apply 将立即调用内部的函数,$apply 将开启调用 $digest,而 $eval 不会所以尽量不要在 digest 外使用,$apply 可以在 digest 使用做到了将外部代码合并到 digest 循环中。
  2. $evalAsync, $applyAsync, 和 $$postDigest 延迟调用代码,每一个都对应一个数组(消息队列),其中$$postDigest 最为特殊因为它触发在 digest 之后导致它所触发 scope 上的改变需要手动去调用 $digest。