chiyan-lin / Blog

welcome gaidy Blog
6 stars 3 forks source link

angular 脏检测基本原理 #8

Open chiyan-lin opened 3 years ago

chiyan-lin commented 3 years ago
<div>
  <button type="button" ng-click="increase">增加</button>
  <button type="button" ng-click="decrease">减少</button> 数量:
  <span ng-bind="data"></span>
</div>

// angular 脏检查基本实现
window.onload = function () {
  function Scope () {
    this.$$watchList = [];
  }

  // 获取最新的 scope 的值
  Scope.prototype.getNewValue = function () {
    return $scope[this.name];
  }

  // 设置一个订阅方式
  Scope.prototype.$watch = function (name, listener) {
    var watch = {
      name: name,
      getNewValue: this.getNewValue,
      listener: listener || function () { }
    };
    // 将需要被观察的对象推进入 wl 中
    this.$$watchList.push(watch);
  }

  // 触发脏检查的方法
  Scope.prototype.$digest = function () {
    var dirty = true;
    var checkTimes = 0;
    while (dirty) {
      dirty = this.$$digestOnce();
      checkTimes++;
      // 设置一个limit防止因为 scope 的相互改变触发无限循环
      if (checkTimes > 10 && dirty) {
        throw new Error("循环过多");
      }
    }
  }

  // 遍历的核心是值的对比
  // 每次触发 digest 会对所有订阅进行遍历【所以数据太多太大性能上就有问题】
  // 在发现数据已经变化之后,会返回 true,然后会再次进行一次遍历直到确认没有改变了
  // 这里如果是引用类型对比就会有问题
  Scope.prototype.$$digestOnce = function () {
    var dirty;
    var list = this.$$watchList;
    for (var i = 0; i < list.length; i++) {
      var watch = list[i];
      var newValue = watch.getNewValue();
      var oldValue = watch.last;
      if (newValue !== oldValue) {
        watch.listener(newValue, oldValue);
        dirty = true;
      } else {
        dirty = false;
      }
      watch.last = newValue;
    }
    return dirty;
  }

  // 初始化下这个 Scope
  var $scope = new Scope();
  $scope.sum = 0;
  $scope.data = 0;
  $scope.increase = function () {
    this.data++;
  };
  $scope.decrease = function () {
    this.data--;
  };
  $scope.equal = function () {

  };
  // 页面进行 ng-bind 的变量会被 watch
  $scope.$watch('data', function (newValue, oldValue) {
    $scope.sum = newValue * 666;
    console.log("new: " + newValue + "old: " + oldValue);
  });

  // 绑定的方法
  function bind () {
    const list = document.querySelectorAll('[ng-click]');
    for (let i = 0, l = list.length; i < l; i++) {
      list[i].onclick = function (index) {
        return function () {
          const func = this.getAttribute('ng-click');
          $scope[func]();
          // 在每次点击的时候就会执行一次脏检查,再进行一次页面更新
          $scope.$digest();
          apply();
        }
      }
    }
  }

  // 渲染函数
  function apply () {
    const list = document.querySelectorAll('[ng-bind]');
    for (var i = 0, l = list.length; i < l; i++) {
      const bindData = list[i].getAttribute('ng-bind');
      list[i].innerHTML = $scope[bindData];
    }
  }

  bind();
  // 手动触发一次脏检测
  // $scope.$digest();
  // apply();
}

直接上代码,基本上注释已经把代码的基本功能解释清楚了,

想要实现双向绑定,就在输入框绑定一个 input ,然后每次执行完执行一次脏检查处理最新值

其他框架的比较

React ,它采用的是虚拟DOM,简单来说就是将页面上的DOM和JS里面的虚拟DOM进行对比,然后将不一样的地方渲染到页面上去。

其实这个思想就是AngularJS的脏检查机制,只不过AngularJS是检查的数据,React是检查的虚拟DOM而已。

vue 是使用 Object.defineProperty 来劫持对象属性的 getter/setter ,从而进行视图更新等功能。

那么这两种检查数据是否变化的方式哪种比较好呢

还是要看场景~~~

对于数据变更比较频繁的场景,比如

function Ctrl($scope){
   var  list = [];
   $scope.checkedItemsNumber = 0;
   for(var i = 0;i<1000;i++){
    list.push(false);
   } 
   $scope.toggleChecked = function(flag){
    for(var i = 0,l= list.length;i++){
        list[i] = flag;
        $scope.checkedItemsNumber++;
    }
   }
}

脏检查是数据完全改变完成再进行 diff 的,

但是使用 getter/setter 会在每次数据都变化的时候触发


AngularJS双向绑定之脏检查机制