scopes的作用

  • 在 controller,directive 和 template 之间共享 scopes 中的数据。
  • 不同的 scopes 之间传递数据。
  • 广播和监听事件。
  • 监听数据变化。

在 AngularJS 中,引入了数据的脏检查来进行上面的第四点的实现。

Scope 上的数据

  1. const scope = new Scope();
  2. scope.aProperty = 1;

这就是 Scope 上的数据如何工作的,只是普通的 js 对象上的属性,没有特殊的 setter 没有特殊的限制,Scope 发挥作用的秘诀在于 $watch 和 $digest 。

$watch 和 $digest

他们两个组成了一个循环,响应数据的变化,使用$watch,来创建一个叫做 watcher 的东西。watcher 可以监听 scope 上数据的变化,$watch 应该接受两个函数,

  • watch 函数, 用来监听数据
  • listener 函数,当被监听的数据变化时调用

$digest 函数,他会遍历 scope 上所有的 watcher ,然后找到变化了的数据来顺序运行他们的 watcher 和 listener。

  1. 'use strict';
  2. function Scope() {
  3. this.$$watcher = [];
  4. }
  5. module.exports = Scope;

我们需要一个东西来存放创建的 watcher ,两个 $ 代表着这个属性是框架内部使用的属性,不应该被应用程序所使用。

  1. Scope.prototype.$watch = function(watchFn, listenerFn) {
  2. const watcher = {
  3. watchFn: watchFn,
  4. listenerFn: listenerFn
  5. };
  6. this.$$watchers.push(watcher);
  7. };
  8. Scope.prototype.$digest = function() {
  9. this.$$watchers.forEach(watcher => {
  10. watcher.listenerFn();
  11. });
  12. };

watcher 的作用

watcher 由 $watch 创建,现阶段我们需要手动给 scope 中的每个属性调用 $watch 编写 watchFn,listenerFn 来创建 watcher,在 angularjs 中这个阶段是在 scope 赋值的时候就已经完成。

  • watcher 负责监听数据变化
  • watcher 负责响应数据变化

    $watch 的作用

    给每个 scope 上的数据创建 watcher,关于 watch 的哲学含义,表示 angularjs 盯上你了,没有好果子吃。

    $digest 的作用

    $digest 函数,他会遍历 scope 上所有的 watcher ,然后找到变化了的数据来顺序运行他们的 watcher 和 listener,所以 $digest 根据 watcher 的 watchFn 找到变化数据对应的 watcher,来执行 watcher 的 listenerFn。

    Dirty Checking

    我们现在先对 scope 上的一个数据来编写 watcher

每个 watcher 应该对应 scope 上的一个数据,所以我们需要从 scope 上拿到对应的数据传给 watcher,当数据变化时调用 listenerFn ,最简单的就是之间把 scope 对象传入 watchFn。

  1. function watchFn(scope) {
  2. return scope.firstName;
  3. }

$watcher 创建的 watcher 中的 watch 函数,应该返回变化了的数据,通常这些数据在 scope 上存在,我们可以将 scope 作为参数传入 watch 函数,这样 watch 可以轻易地拿到了数据。最简单的实现就是:

  1. Scope.prototype.$digest = function() {
  2. this.$$watchers.forEach(watcher => {
  3. watcher.watchFn(this);
  4. watcher.listenerFn();
  5. });
  6. };

$digest 的作用是调用 watch 函数,比较返回的值和上次返回的值是否一致,不一致这个 watcher 被标记为脏,调用 listener 函数。

  1. Scope.prototype.$digest = function () {
  2. var self = this;
  3. var newValue, oldValue;
  4. this.$$watchers.forEach(function (watcher) {
  5. newValue = watcher.watchFn(self);
  6. oldValue = watcher.last;
  7. if (newValue !== oldValue) {
  8. watcher.last = newValue;
  9. watcher.listenerFn(newValue, oldValue, self);
  10. }
  11. });
  12. };

last 第一次为 undefined,这样的话如果我们传入的值为 undefined 时第一次 $digest 不会被调用,我们希望第一次总会触发数据的 listenerFn。

  1. function Scope() {
  2. this.$$watchers = [];
  3. }
  4. function initWatchVal() {}
  5. Scope.prototype.$watch = function (watchFn, listenerFn) {
  6. const watcher = {
  7. watchFn: watchFn,
  8. listenerFn: listenerFn,
  9. last: initWatchVal
  10. };
  11. this.$$watchers.push(watcher);
  12. };

这样的就实现了我们的目的,并且让 $digest 让 listenerFn 在第一次时以新值执行

  1. Scope.prototype.$digest = function () {
  2. this.$$watchers.forEach((watcher) => {
  3. let newValue, oldValue;
  4. newValue = watcher.watchFn(this);
  5. oldValue = watcher.last;
  6. if (newValue !== oldValue) {
  7. watcher.last = newValue;
  8. watcher.listenerFn(newValue,
  9. (oldValue === initWatchVal ? newValue : oldValue),
  10. this);}
  11. })
  12. }

数据和 watcher

  • 绑定 watcher 而不是绑定数据,只有数据没有 watcher 没有作用
  • angularjs 只是在遍历 watcher
  • 每个 watcher 是独立的,包含了对应数据如何是否变化和怎么更新
  • watchFn 只会在 $digest 中被调用

    脏检查

    如果 scope 上的一个数据被改变后,有可能在它的 listenerFn 改变 scope 上的其他数据,如果已经遍历过那个 watcher 那么就会跳过这次更改。
    1. it('triggers chained watchers in the same digest', function() {
    2. const scope = new Scope();
    3. scope.name = 'Jane';
    4. scope.$watch(
    5. function(scope) { return scope.nameUpper; },
    6. function(newValue, oldValue, scope) {
    7. if (newValue) {
    8. scope.initial = newValue.substring(0, 1) + '.';
    9. }
    10. })
    11. scope.$watch(
    12. function(scope) { return scope.name; },
    13. function(newValue, oldValue, scope) {
    14. if (newValue) {
    15. scope.nameUpper = newValue.toUpperCase();
    16. }
    17. }
    18. );
    19. scope.$digest();
    20. expect(scope.initial).to.equals('J.');
    21. scope.name = 'Bob';
    22. scope.$digest();
    23. expect(scope.initial).to.equals('B.');
    24. })
    给 scope 上的 name 和 nameUpper 创建 watcher,initial 和 nameUpper 依赖于 name 属性,如果我们调换次序程序刚好可以通过,但是 watcher 之间的依赖顺序不取决于 digest 顺序。 ```jsx Scope.prototype.$digest = function () { let dirty; do {
    1. dirty = this.$$digestOnce();
    } while (dirty); }

Scope.prototype.$$digestOnce = function () { let newValue, oldValue, dirty; this.$$watchers.forEach((watcher) => { newValue = watcher.watchFn(this); oldValue = watcher.last; if (newValue !== oldValue) { watcher.last = newValue; watcher.listenerFn(newValue, (oldValue === initWatchVal ? newValue : oldValue), this); dirty = true; } }) return dirty; }

  1. 只要有数据变化就**将整个 scope 标记为 dirty **,就执行一遍所有的 watcher ,直到所有的所有的数据都没有发生改变为止,所以在一次 digest 中每个 watcher 也许会执行不止一次,所以我们说 listenerFn 要幂等的,没有副作用,或者副作用执行有限次。
  2. <a name="tPX3A"></a>
  3. ## TTL-“Time To Live”
  4. ```jsx
  5. Scope.prototype.$digest = function () {
  6. let ttl = 10;
  7. let dirty;
  8. do {
  9. dirty = this.$$digestOnce();
  10. if (dirty && !(ttl--)) {
  11. throw ('10 digest iterations reached');
  12. }
  13. } while (dirty);
  14. }

减少 $$watcher 遍历次数

  1. it('ends the digest when the last watch is clean', function () {
  2. const scope = new Scope();
  3. scope.array = _.range(100);
  4. let watchExecutions = 0;
  5. _.times(100, function (i) {
  6. scope.$watch(
  7. function (scope) {
  8. watchExecutions++;
  9. return scope.array[i];
  10. },
  11. function (newValue, oldValue, scope) {
  12. }
  13. );
  14. });
  15. scope.$digest();
  16. expect(watchExecutions).to.equals(200);
  17. scope.array[0] = 420;
  18. scope.$digest();
  19. expect(watchExecutions).to.equals(301);
  20. })

如果一个数据变化了,那么将会遍历 100 个 watcher ,并且标记 scope 为脏,第二次遍历时,只要我们找到上次最后为脏的 watcher,那么是等同于遍历整个数组的,建立 100 个 watcher,第一次 digest 执行 200 次,第二次我们希望它执行 301 次而不是 400 次。

每次数据变化调用 digest 会触发两次 digestOnce ,第一次不能优化始终会遍历完整个 watchers ,我们要优化的是第二轮 digestOnce ,每次 digest 开始会将 lastDirtyWatch 清空,假设我们改变了第 10 个数据,在数据改变后执行一轮 digest ,就标记第 10 个 watcher 为脏 watcher 同时标记这个 scope 为脏,如果这个 watcher 的 listnerFn 改变了第 10 个之前的数据,那么第二轮 digest 自然会遍历对应的 watcher,如果这个 watcher 的 listnerFn 改变了第 10 个之后的数据,那么第一轮 digest 还会继续遍历并且更新 $$lastDirtyWatch 所以不用担心。

  1. Scope.prototype.$$digestOnce = function() {
  2. const self = this;
  3. let newValue, oldValue, dirty;
  4. _.forEach(this.$$watchers, function(watcher) {
  5. newValue = watcher.watchFn(self);
  6. oldValue = watcher.last;
  7. if (newValue !== oldValue) {
  8. self.$$lastDirtyWatch = watcher;
  9. watcher.last = newValue;
  10. watcher.listenerFn(newValue,
  11. (oldValue === initWatchVal ? newValue : oldValue),
  12. self);
  13. dirty = true;
  14. } else if (self.$$lastDirtyWatch === watcher) {
  15. return false;
  16. }
  17. })
  18. return dirty;
  19. }

使用 lodash 的 forEach ,这样 return 的时候会跳出循环。

引用类型数据

  1. Scope.prototype.$$areEqual = function(newValue, oldValue, valueEq) {
  2. if (valueEq) {
  3. return _.isEqual(newValue, oldValue);
  4. } else {
  5. return newValue === oldValue;
  6. }
  7. };

angularjs 内部有自己的比较函数,我们用 lodash 的即可,而且为了察觉到旧值的变化,我们也需要进行一次深拷贝,否则两个值会同时变化,同样地 angularjs 也有自己的深拷贝函数,我们也用 lodash 的。

  1. watcher.last = (watcher.valueEq ? _.cloneDeep(newValue) : newValue);

$watch 接受第三个参数作为是否开启深度比较的标志

NaN

如果我们不处理 NaN 那么 scope 永远为脏,我们可以在 $$areEqual 中处理

  1. Scope.prototype.$$areEqual = function(newValue, oldValue, valueEq) {
  2. if (valueEq) {
  3. return _.isEqual(newValue, oldValue);
  4. } else {
  5. return newValue === oldValue ||
  6. (typeof newValue === 'number' && typeof oldValue === 'number' &&
  7. isNaN(newValue) && isNaN(oldValue));
  8. }
  9. };

错误处理

现在的代码出错后会停下,实际上代码应该继续向下走,angularjs 有 $exceptionHandler 来处理错误,我们简单处理即可,出错的地方无非就是 watchFn 和 listenerFn,让其中一个 watcher 出错剩下的 watcher 继续遍历即可。

  1. Scope.prototype.$$digestOnce = function() {
  2. const self = this;
  3. let newValue, oldValue, dirty;
  4. _.forEach(this.$$watchers, function(watcher) {
  5. try {
  6. newValue = watcher.watchFn(self);
  7. oldValue = watcher.last;
  8. if (!self.$$areEqual(newValue, oldValue, watcher.valueEq)) {
  9. self.$$lastDirtyWatch = watcher;
  10. watcher.last = (watcher.valueEq ? _.cloneDeep(newValue) : newValue);
  11. watcher.listenerFn(newValue,
  12. (oldValue === initWatchVal ? newValue : oldValue),
  13. self);
  14. dirty = true;
  15. } else if (self.$$lastDirtyWatch === watcher) {
  16. return false;
  17. }
  18. } catch (err) {
  19. console.log(err);
  20. }
  21. })
  22. return dirty;
  23. }

销毁本身的 watcher

只要 scope 存在,watcher 一般是不会销毁的,但是如果有需求在 scope 存在时销毁一个 watcher 怎么办,在angularjs 中非常巧妙的实现了,就是利用 $watch 来返回一个函数来控制相应 watcher。

  1. it('allows destroying a $watch during digest', function () {
  2. const scope = new Scope();
  3. scope.aValue = 'abc';
  4. const watchCalls = [];
  5. scope.$watch(
  6. function (scope) {
  7. watchCalls.push('first');
  8. return scope.aValue;
  9. }
  10. );
  11. const destroyWatch = scope.$watch(
  12. function (scope) {
  13. watchCalls.push('second');
  14. destroyWatch();
  15. }
  16. );
  17. scope.$watch(
  18. function (scope) {
  19. watchCalls.push('third');
  20. return scope.aValue;
  21. }
  22. );
  23. scope.$digest();
  24. console.log(watchCalls);
  25. expect(watchCalls).to.equals(['first', 'second', 'third', 'first', 'third']);
  26. });

[‘first’, ‘second’, ‘first’, ‘third’, ‘first’, ‘third’] 不做修改的话会得到这样一个数组,由于第一次 digest 就删除了第二个 watcher,导致指针偏移,所以第一轮的第三个 watcher 不会被遍历到,如果有第四个 watcher 就会遍历执行第四个 watcher ,解决办法很简单,将每次新的 watcher 放在起点,并且从反方向遍历数组,这样的话不会影响剩下的数组

  1. for(let i = arr.length-1;i >= 0;i--) {} //arr.length初始化后就不会改变
  1. [1,2,3,4] i = length-1(3)
  2. [1,2,4] i = 3 - 1(2) 遍历并删掉第三个元素
  3. [1,2,4] i = 3 -2(1)

因为 splice 数组总是会移动后面的元素,所以当 watcher 被移除,被 shift 的是已经被处理的部分,而之前的被 shift 的是没有被处理的部分。

在销毁另一个 watcher

  1. it('allows a $watch to destroy another during digest', function () {
  2. const scope = new Scope();
  3. scope.aValue = 'abc';
  4. scope.counter = 0;
  5. scope.$watch(
  6. function (scope) {
  7. return scope.aValue;
  8. },
  9. function (newValue, oldValue, scope) {
  10. destroyWatch();
  11. }
  12. );
  13. const destroyWatch = scope.$watch(
  14. function (scope) {
  15. },
  16. function (newValue, oldValue, scope) {
  17. }
  18. );
  19. scope.$watch(
  20. function (scope) {
  21. return scope.aValue;
  22. },
  23. function (newValue, oldValue, scope) {
  24. scope.counter++;
  25. }
  26. );
  27. scope.$digest();
  28. expect(scope.counter).to.equals(1);
  29. });

第一轮遍历,会执行第一个 watcher 的 listenerFn 销毁第二个 watcher ,[3,2,1],[3,1],第一个 watcher 往下移,导致 lastDirtyWatcher 还是它所以提前结束了,第三个永远没有执行,只需要在每次 splice 都将 lastDirtyWatcher 清零即可。

销毁所有 watcher

  1. it('allows destroying several $watches during digest', function () {
  2. const scope = new Scope();
  3. scope.aValue = 'abc';
  4. scope.counter = 0;
  5. const destroyWatch1 = scope.$watch(
  6. function (scope) {
  7. destroyWatch1();
  8. destroyWatch2();
  9. }
  10. );
  11. const destroyWatch2 = scope.$watch(
  12. function (scope) {
  13. return scope.aValue;
  14. },
  15. function (newValue, oldValue, scope) {
  16. scope.counter++;
  17. }
  18. );
  19. scope.$digest();
  20. expect(scope.counter).toBe(0);
  21. });

在第一次 watcher 的时候就把 $watch 注册的所以 watcher 删掉,自然不会执行其他 watcher,但是循环会继续遍历我们也不希望它报错,所以:

  1. Scope.prototype.$$digestOnce = function () {
  2. const self = this;
  3. let newValue, oldValue, dirty;
  4. _.forEachRight(this.$$watchers, function (watcher) {
  5. try {
  6. if (watcher) {
  7. newValue = watcher.watchFn(self);
  8. oldValue = watcher.last;
  9. if (!self.$$areEqual(newValue, oldValue, watcher.valueEq)) {
  10. self.$$lastDirtyWatch = watcher;
  11. watcher.last = (watcher.valueEq ? _.cloneDeep(newValue) : newValue);
  12. watcher.listenerFn(newValue,
  13. (oldValue === initWatchVal ? newValue : oldValue),
  14. self);
  15. dirty = true;
  16. } else if (self.$$lastDirtyWatch === watcher) {
  17. return false;
  18. }
  19. }
  20. } catch (err) {
  21. console.log(err);
  22. }
  23. })
  24. return dirty;
  25. }

小结

  • $watch 的哲学含义:观测
  • 脏检查:检查到 scope 不为脏为止
  • TTL 和 最后脏 watcher 来减少遍历 watcher 个数
  • 销毁 watcher 只是简单的 splice,但是引发的改变 $$watchers 的 shift 副作用会比较难处理