资讯专栏INFORMATION COLUMN

Angular 学习笔记:$digest 实现原理

baiy / 2994人阅读

摘要:前缀表示私有变量上述代码实现的并不实用,因为实际上我们需要的是监听的对象数据发生改变时才执行相应的方法。我们使用来约束遍历的最大次数,在中默认次数为。

$watch 和 $digest

$watch$digest 是数据绑定中的核心概念:我们可以使用 $watch 在 scope 中绑定 watcher 用于监听 scope 中发生的变化,而 $digest 方法的执行即是遍历 scope 上绑定的所有 watcher,并执行相应的 watch(指定想要监控的对象) 和 listener(当数据改变时触发的回调) 方法。

function Scope {
    this.$$watchers = []; // $$ 前缀表示私有变量
}

Scope.prototye.$watch = function(watchFn, listenerFn) {
    let watcher = {
        watchFn: watchFn,
        listenerFn: listenerFn,
    };

    this.$$watchers.push(watcher);
}

Scope.prototype.$digest = function() {
    this.watchers.forEach((watcher) => {
        watcher.listenerFn();
    });
}

上述代码实现的 $digest 并不实用,因为实际上我们需要的是:监听的对象数据发生改变时才执行相应的 listener 方法

脏检查
Scope.prototype.$digest = function() {
    let self = this;
    let newValue, oldValue;
    this.watchers.forEach((watcher) => {
        newValue = watcher.watchFn(self);
        oldValue = watcher.last;
        if (newValue !== oldValue) {
            watch.last = newValue;
            watcher.listenerFn(newValue, oldValue, self);
        }
    });
}

上述代码在大部分情况下可以正常运行,但是当我们首次遍历 watcher 对象时其 last 变量值为 undefined,这样会导致如果 watcher 的第一个有效值同为 undefined 不会触发 listener 方法。

console.log(undefined === undefined) // true

我们使用 initWatchVal 方法解决这个问题.

function initWatchVal() {
  // TODO
}

Scope.prototye.$watch = function(watchFn, listenerFn) {
  let watcher = {
      watchFn: watchFn,
      listenerFn: listenerFn || function() {},
      last: initWatchVal
  };

  this.$$watchers.push(watcher);
}

Scope.prototype.$digest = function() {
  let self = this;
  let newValue, oldValue;
  this.watchers.forEach((watcher) => {
      newValue = watcher.watchFn(self);
      oldValue = watcher.last;
      if (newValue !== oldValue) {
          watch.last = newValue;
          watcher.listenerFn(newValue, oldValue === initWatchVal ? newValue : oldValue, self);
      }
  });
}
循环进行脏检查

在进行 digest 时往往会发生如下情况,即某个 watcher 执行 listener 方法会引起其他 watcher 监听的对象数据发生改变,因此我们需要循环进行脏检查来使变化“彻底”完成。

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

Scope.prototype.$digest = function() {
    let dirty;
    do { dirty = this.$$digestOnce(); }
    while (dirty);
}

上述代码只要在遍历中发现脏值,就会多循环一轮直到没有发现脏值为止,我们考虑这样的情况:即是两个 watcher 之间互相影响彼此,则会导致无限循环的问题。

我们使用 TTL(Time to Live)来约束遍历的最大次数,在 Angular 中默认次数为10。

Scope.prototype.$digest = function() {
    let dirty;
    let ttl = 10;
    do {
        dirty = this.$$digestOnce();
        if (dirty && !(ttl--)) {
            throw "10 digest iterations reached.";
        }
    } while (dirty)
}

同时,在每次 digest 的最后一轮遍历没有必要对全部 watcher 进行检查,我们通过使用 $$lastDirtyWatch 变量来对这部分代码的性能进行优化。

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

Scope.prototype.$digest = function() {
    let dirty;
    let ttl = 10;

    this.$$lastDirtyWatch = null;

    do {
        dirty = this.$$digestOnce();
        if (dirty && !(ttl--)) {
            throw "10 digest iterations reached.";
        }
    } while (dirty)
}

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

    return dirty;
}

同时为了避免 $watch 嵌套使用带来的不良影响,我们需要在每次添加 watcher 时重置 $$lastDirtyWatch:

Scope.prototye.$watch = function(watchFn, listenerFn) {
    let watcher = {
        watchFn: watchFn,
        listenerFn: listenerFn || function() {},
        last: initWatchVal
    };

    this.$$watchers.push(watcher);
    this.$$lastDirtyWatch = null;
}
深浅脏检查

目前为止我们实现的脏检查,仅能监听到值的变化(浅脏检查),无法判断引用内部数据发生的变化(深脏检查)。

Scope.prototye.$watch = function(watchFn, listenerFn, valueEq) {
    let watcher = {
        watchFn: watchFn,
        listenerFn: listenerFn || function() {},
        valueEq: !!valueEq,
        last: initWatchVal
    };

    this.$$watchers.push(watcher);
    this.$$lastDirtyWatch = null;
}
Scope.prototype.$$areEqual = function(newValue, oldValue, valueEq) {
  if (valueEq) {
      return _.isEqual(newValue, oldValue);
  } else {
      return newValue === oldValue;
  }
}
Scope.prototype.$$digestOnce = function() {
  let self = this;
  let newValue, oldValue, dirty;
  this.watchers.forEach((watcher) => {
      newValue = watcher.watchFn(self);
      oldValue = watcher.last;
      if (!self.$$areEqual(newValue, oldValue, watcher.valueEq)) {
          self.$$lastDirtyWatch = watcher;
          dirty = true;
          watch.last = watcher.valueEq ? _.cloneDeep(newValue) : newValue;
          watcher.listenerFn(newValue, oldValue === initWatchVal ? newValue : oldValue, self);
      } else if (self.$$lastDirtyWatch === watcher) {
          return false;
      }
  });

  return dirty;
}
NaN 的兼容考虑

需要注意的是,NaN 不等于其自身,所以在判断 newValue 与 oldValue 是否相等时,需要特别考虑。

Scope.prototype.$$areEqual = function(newValue, oldValue, valueEq) {
    if (valueEq) {
        return _.isEqual(newValue, oldValue);
    } else {
        return newValue === oldValue ||
            (typeof newValue === "number" && typeof oldValue === "number" && isNaN(newValue) && isNaN(oldValue));
    }
}

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/84389.html

相关文章

  • [译] $digestAngular 中重生

    摘要:但如果一个组件在生命周期钩子里改变父组件属性,却是可以的,因为这个钩子函数是在更新父组件属性变化之前调用的注即第步,在第步之前调用。 原文链接:Angular.js’ $digest is reborn in the newer version of Angular showImg(https://segmentfault.com/img/remote/146000001468785...

    incredible 评论0 收藏0
  • [译] 关于 `ExpressionChangedAfterItHasBeenCheckedErro

    摘要:本文将解释引起这个错误的内在原因,检测机制的内部原理,提供导致这个错误的共同行为,并给出修复这个错误的解决方案。这一次过程称为。这个程序设计为子组件抛出一个事件,而父组件监听这个事件,而这个事件会引起父组件属性值发生改变。 原文链接:Everything you need to know about the ExpressionChangedAfterItHasBeenCheckedE...

    andong777 评论0 收藏0
  • angular1学习笔记,view model 的同步过程

    摘要:但实际上这时程序并没有计算手续费。经过排查并查阅文档之后,发现是的问题。本文没有具体介绍和管道,关于这部分可以参考文中给出的链接 事情起源于在项目中遇到的一个小问题:项目中需要一个输入框输入卖出产品数量,并且在用户输入后根据输入数据计算手续费。很自然的我用了ng-model和ng-change,并且一般情况下没什么问题。问题是:输入框下还有一个按钮是全部卖出,点击这个按钮程序会自动设置...

    Forelax 评论0 收藏0
  • 面试题总结

    摘要:异步管理等到执行完成后返回种状态,代表成功,代表失败。我们在函数内声明的变量叫局部变量,局部变量只能在里面访问,外面是访问不到的。那么就是为解决这一问题的。可以用链式写法等到异步有结果再进行下一步。 1. vue的双向绑定原理: vue的双向绑定原理是通过Object.definedProperty的getter和setter来对属性进行数据劫持的。 因为Object.definedP...

    weizx 评论0 收藏0
  • 面试题总结

    摘要:异步管理等到执行完成后返回种状态,代表成功,代表失败。我们在函数内声明的变量叫局部变量,局部变量只能在里面访问,外面是访问不到的。那么就是为解决这一问题的。可以用链式写法等到异步有结果再进行下一步。 1. vue的双向绑定原理: vue的双向绑定原理是通过Object.definedProperty的getter和setter来对属性进行数据劫持的。 因为Object.definedP...

    enali 评论0 收藏0

发表评论

0条评论

baiy

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<