什么是脏检查?

脏检查的全名是 脏数据检查。是AngularJS命名的。

脏数据也就是产生了变化的数据。

脏检查因一个原因而被称为脏。它定时检查而不是直接监听属性变化。我们把这个检查称为摘要周期(digest)。

angularJS监测对象变化不是像vue.js那样通过Object.defineproperty这种接口,而是在某些情况下制定策略,通过复制保存一份数据,进行快照对比,来监测变化。

脏检查这个东西,其实在三大主流前端框架中或多或少都有涉及。React 每次生成新的 Virtual DOM,与旧 Virtual DOMdiff 操作本来就可以看做一次脏检查。Vue 从相对彻底的抛弃了脏检查机制,使用 Property 主动触发 UI 更新,但是 Vue 仍然不能抛弃 track by (用来标记数组元素的key) 这个东西。

通过将新旧数组的 track by 元素做 diff 猜测用户的行为,最大可能的减少 DOM 树的操作,这就是 track by的用处。

Angular 1的性能被广为诟病,因为在 Angular 1 的机制下,脏检查的执行范围过大以及频率太过频繁了。

资料一:

AngularJS remembers the value and compares it to a previous value. This is basic dirty-checking. If there is a change in value, then it fires the change event.

资料二:

Angular defines a concept of a so called digest cycle. This cycle can be considered as a loop, during which Angular checks if there are any changes to all the variables watched by all the $scopes. So if you have $scope.myVar defined in your controller and this variable was marked for being watched, then you are explicitly telling Angular to monitor the changes on myVar in each iteration of the loop.

资料三:

1292490-20180608145022878-1851524743.png

资料四:

angular中变量是双向绑定的 ,那么怎么知道一个变量是否是变化了呢?

1能通过固定的接口才能改变变量的值,比如说只能通过set()设置变量的值,set被调用的时候比较一下就知道了。这种方法的缺点是写法比较繁琐。

2脏检查,将原对象复制一份快照,在某个时间,比较现在对象与快照的值,如果不一样就表明发生了变化,这个策略要保留两份变量,而且要遍历对象,比较每个属性,这样会有一定的性能问题

angular使用的就是脏检查:

1不会脏检查所有的对象。当对象被绑定到html中后,这个对象才会添加为检查对象(watcher)

2不会脏检查所有的属性,同样当属性被绑定后,这个属性才会被列为检查的属性

在angular程序初始化时,会将绑定的对象的属性添加为监听对象(watcher),也就是说一个对象绑定了N个属性,就会添加N个watcher。

angular什么时候去脏检查呢?angular所系统的方法中都会触发比较事件,比如:controller初始化的时候,所有以ng-开头的事件爱你执行后,都会出发脏检查

资料五:

ng: 是把所有事件都进行了包装,例如把setTimeout包装成$timeout,当这些事件触发后会调用$apply, $apply 会从根也就是$rootScope, $rootScope.$digest()方法会递归把所有的子scope里面的watcher的新旧值来对比(脏检测),如有变动,才会更新那个指令的模板渲染。在ng1版本里面 组件是指令的语法糖,渲染更新也是以指令为单位。

vue: 是在模板渲染的时候,会读取{{}}之中的变量,触发get。把 Dep.target也就是渲染watcher,放在了自己的依赖(Dep实例),dep.subs 数组中。

当哪个数据变化了 就会喊一声(notify),他的订阅者们(subs数组中的warcher们)就会更新( watcher.update() ) , 更新是以组件为单位。

我的总结:

假设教室玻璃碎了,ng是 老师对学生们进行一一排查( 脏检测 ),看看是谁。vue是学生主动站出来,是我,因为我的手破了,啊!(notify)好疼!

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <div>
      <input type="text" ng-bind="name" />
      <button type="button" ng-click="increment">increment</button>
      <div ng-bind="name"></div>
    </div>
    <script>
      class Watcher {
        constructor(name, last, exp, listener) {
          this.name = name; // 数据变量名
          this.last = last; // 数据变量旧值
          this.newVal = exp; // 返回数据变量新值的函数
          this.listener = listener || function () {}; // 监听回调函数,变量“脏”时触发
          this.listener(this.last, this.newVal());
        }
      }

      class Scope {
        constructor() {
          // 观察者数组
          this.watchers = [];
        }

        // 添加数据观察者
        watch(name, exp, listener) {
          this.watchers.push(new Watcher(name, "", exp, listener));
        }

        // 对监视器的新旧值进行对比
        // 当新旧值不同时,调用listener函数进行相应操作
        // 并将旧值更新为新值。它将不断重复这一过程,直到所有数据变量的新旧值相等:
        digest() {
          let dirty = true;
          while (dirty) {
            dirty = false;
            this.watchers.forEach(watcher => {
              let newVal = watcher.newVal();
              var oldVal = watcher.last;
              if (newVal !== oldVal) {
                dirty = true;
                watcher.listener(oldVal, newVal);
                watcher.last = newVal;
              }
            });
          }
        }
      }

      class App extends Scope {
        name = "JoyNop";

        constructor() {
          super();
        }

        increment() {
          this.name += "+";
        }
      }

      const app = new App();
      run(app);
      function run(app) {
        document // 绑定依赖观察者
          .querySelectorAll("[ng-bind]")
          .forEach(it => {
            const nodeName = it.nodeName.toLowerCase();
            const bindKey = it.getAttribute("ng-bind");
            if (bindKey in app) {
              app.watch(
                bindKey,
                () => app[bindKey],
                (oldVal, newVal) => {
                  if (nodeName === "input") {
                    it.value = newVal;
                  } else {
                    it.textContent = newVal;
                  }
                }
              );
            }
          });

        // 绑定事件
        document.querySelectorAll("[ng-click]").forEach(it => {
          const bindKey = it.getAttribute("ng-click");
          it.addEventListener("click", e => {
            if (app[bindKey] && typeof app[bindKey] === "function") {
              app[bindKey]();
              app.digest();
            }
          });
        });

        // 双向绑定
        document.querySelectorAll("input[ng-bind]").forEach(it => {
          const bindKey = it.getAttribute("ng-bind");
          it.addEventListener("input", e => {
            app[bindKey] = it.value;
            app.digest();
          });
        });
      }
    </script>
  </body>
</html>

监听object和array的修改

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <div>
      <input type="text" ng-bind="name" />
      <button type="button" ng-click="increment">increment</button>
      <div ng-bind="name"></div>
      <hr />
      <div ng-bind="obj"></div>
      <button ng-click="changeValue">改变object的值</button>
    </div>
    <script>
      function equal(obj, other) {
        const objectTag = "[object Object]";
        const arrayTag = "[object Array]";
        const _tostring = value => Object.prototype.toString.call(value);
        const emptyp = value => JSON.stringify(value).length === 2;
        function Equal(obj, other) {
          let objTag = _tostring(obj);
          let otherTag = _tostring(other);

          // 非集合,使用===判断
          if (
            objTag !== objectTag &&
            objTag !== arrayTag &&
            otherTag !== objectTag &&
            otherTag !== arrayTag
          ) {
            return obj === other;
          }

          // 集合类型不一样
          if (objTag !== otherTag) return false;

          // 集合元素数量不一样
          if (
            Object.getOwnPropertyNames(obj).length !==
            Object.getOwnPropertyNames(other).length
          )
            return false;

          // 类型一样的空集合,永远相等。
          if (emptyp(obj) && emptyp(other)) return true;

          let rsult = false;
          for (const k in obj) {
            if (k in other) {
              const obj_value = obj[k];
              const other_value = other[k];
              rsult = Equal(obj_value, other_value);
            } else {
              return false;
            }
          }
          return rsult;
        }

        return Equal(obj, other);
      }
      function copytree(tree, all = true) {
        const objectTag = "[object Object]";
        const arrayTag = "[object Array]";

        const _tostring = value => Object.prototype.toString.call(value);
        // 记录所有的对象
        const map = new WeakMap();
        function copyTree(tree, all = true) {
          const treeTag = _tostring(tree);
          const res =
            treeTag === objectTag ? {} : treeTag === arrayTag ? [] : tree;

          if (treeTag !== objectTag && treeTag !== arrayTag) return res;

          // 判断是否有此对象
          if (map.has(tree)) {
            // 直接返回
            return tree;
          } else {
            map.set(tree, true);
          }

          const t = all ? Object.getOwnPropertyNames(tree) : tree;

          if (all) {
            for (const i in t) {
              const k = t[i];
              res[k] = copyTree(tree[k], all);
            }
          } else {
            for (const k in t) {
              res[k] = copyTree(tree[k], all);
            }
          }

          return res;
        }

        return copyTree(tree, all);
      }
      function evalFun(bindKey, data) {
        try {
          const r = Function(`with(this){ return ${bindKey} }`).apply(
            data,
            arguments
          );
          return r === "" ? undefined : r;
        } catch (error) {}
      }
      function setData(key, newValue, context) {
        return Function(`return function(d) {
        with(this){
          ${key} = d;
        }
      }`)().call(context, newValue);
      }

      class Watcher {
        constructor(last, exp, listener, valueEq) {
          this.last = last; // 数据变量旧值
          this.newVal = exp; // 返回数据变量新值的函数
          this.listener = listener || function () {}; // 监听回调函数,变量“脏”时触发
          this.valueEq = valueEq;
          this.listener(this.last, this.newVal());
        }
      }

      class Scope {
        constructor() {
          // 观察者数组
          this.watchers = [];
        }

        // 添加数据观察者
        // valueEq检查值,而不是引用
        watch(v, exp, listener, valueEq = false) {
          this.watchers.push(
            new Watcher(valueEq ? copytree(v) : v, exp, listener, valueEq)
          );
        }

        // 对监视器的新旧值进行对比
        // 当新旧值不同时,调用listener函数进行相应操作
        // 并将旧值更新为新值。它将不断重复这一过程,直到所有数据变量的新旧值相等:
        digest() {
          let dirty = true;
          // while (dirty) {
          //   dirty = false;
          this.watchers.forEach(watcher => {
            const newVal = watcher.newVal();
            const oldVal = watcher.last;
            if (!this.valueEqual(newVal, oldVal, watcher.valueEq)) {
              dirty = true;
              watcher.listener(oldVal, newVal);
              watcher.last = watcher.valueEq ? copytree(newVal) : newVal;
            }
          });
          // }
        }

        valueEqual(newValue, oldValue, valueEq) {
          if (this.valueEq) {
            return equal(newValue, oldValue);
          } else {
            return newValue === oldValue;
          }
        }
      }

      class App extends Scope {
        name = "JoyNop";
        obj = {
          value: "hello world"
        };

        constructor() {
          super();
        }

        increment() {
          this.name += "+";
        }

        changeValue() {
          this.obj.value = "hello JoyNop";
        }
      }

      const app = new App();
      run(app);
      function run(app) {
        document // 绑定依赖观察者
          .querySelectorAll("[ng-bind]")
          .forEach(it => {
            const nodeName = it.nodeName.toLowerCase();
            const bindKey = it.getAttribute("ng-bind");
            const v = evalFun(bindKey, app);
            if (v) {
              app.watch(
                v,
                () => evalFun(bindKey, app),
                (oldVal, newVal) => {
                  if (nodeName === "input") {
                    it.value = newVal;
                  } else {
                    if (typeof newVal === "object") {
                      it.textContent = JSON.stringify(newVal);
                    } else {
                      it.textContent = newVal;
                    }
                  }
                },
                typeof v === "object" && v !== null
              );
            }
          });

        // 绑定事件
        document.querySelectorAll("[ng-click]").forEach(it => {
          const bindKey = it.getAttribute("ng-click");
          const fn = evalFun(bindKey, app);
          if (fn && typeof fn === "function") {
            it.addEventListener("click", e => {
              fn.call(app);
              app.digest();
            });
          }
        });

        // 双向绑定
        document.querySelectorAll("input[ng-bind]").forEach(it => {
          const bindKey = it.getAttribute("ng-bind");
          it.addEventListener("input", e => {
            setData(bindKey, it.value, app);
            app.digest();
          });
        });
      }
    </script>
  </body>
</html>

参考https://www.cnblogs.com/xuezhi/p/4897831.html