9.6. 手写一个angularjs

302 阅读2分钟

vue,angularjs,react

ps:angularjs与angular区别很大,这里指的angularjs

作为前端三大框架,经常用来对比,我来谈谈我的看法 。
对于使用者来说,我们都是不用操作dom,利用数据变化来更新视图,但是他们是有本质的区别的。

  • 数据驱动: react数据变化需要调用函数setState/useState第二个函数 来更新,可以说是一种被动的视图更新,而angularjs和vue我们直接给响应式数据赋值即可。
  • react : 数据变化了,被动触发视图更新后,它会从根节点,深度遍历所有节点找出变化的dom。当然react做了很多优化,这个遍历比较是可以中断的,避免影响浏览器的渲染,造成长时间的白屏。找到变化的dom通过链接连接起来,然后进行dom的更新。
  • vue :vue的每个响应式的数据都有一个dep订阅中心,每个订阅者watcher订阅它,当数据变化,它会通知订阅的watcher去做视图的更新,这里借用了object.defineproperty方法,让数据在get的时候进行watcher收集,set的时候派发更新。vue的视图更新是以组件的维度,也就是父组件更新可以不会触发子组件的更新。
  • angularjs : 每个响应式数据对应一个watcher对象,它结构如下,watchFn返回需要观测的值,listenerFn返回数据变化需要执行的动作。angularjs重写了浏览器所有会引起数据变化的方法,执行到这些方法,会触发angular对所有的watcher检测,达到数据变化驱动视图。
    var watcher = {
      watchFn: watchFn,
      listenerFn: listenerFn || function() { },
      valueEq: !!valueEq
    }
    

响应式原理

可以改变状应用程序状态更改由以下情况引起:

  • Events - 用户事件比如 clickchangeinputsubmit, …
  • XMLHttpRequests - 比如从远程服务器获取数据
  • Timers - setTimeout()setInterval()

这里要提到zone.js:zone.js采用猴子补丁(Monkey-patched)的暴力方式将JavaScript中的异步任务都包裹了一层,使得这些异步任务都将运行在zone的上下文中。 并且zone.js对JavaScript中的大多数异步事件都做了包裹封装,它们包括:

  • zone.alert;
  • zone.prompt;
  • zone.requestAnimationFrame、zone.webkitRequestAnimationFrame、zone.mozRequestAnimationFrame;
  • zone.addEventListener;
  • zone.addEventListener、zone.removeEventListener;
  • zone.setTimeout、zone.clearTimeout、zone.setImmediate;
  • zone.setInterval、zone.clearInterval

以及对promise、geolocation定位信息、websocket等也进行了包裹封装,你可以在这里找到它们github.com/angular/zon…

更多用法见: github.com/angular/zon…

当我们执行这些浏览器api时会被劫持,触发下面的$digest()方法。

视图更新

function Scope() {
  this.$$watchers = [];
  this.$$asyncQueue = [];
  this.$$postDigestQueue = [];
  this.$$phase = null;
}

Scope.prototype.$beginPhase = function(phase) {
  if (this.$$phase) {
    throw this.$$phase + ' already in progress.';
  }
  this.$$phase = phase;
};

Scope.prototype.$clearPhase = function() {
  this.$$phase = null;
};

Scope.prototype.$watch = function(watchFn, listenerFn, valueEq) {
  var self = this;
  var watcher = {
    watchFn: watchFn,
    listenerFn: listenerFn || function() { },
    valueEq: !!valueEq
  };
  self.$$watchers.push(watcher);
  return function() {
    var index = self.$$watchers.indexOf(watcher);
    if (index >= 0) {
      self.$$watchers.splice(index, 1);
    }
  };
};

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));
  }
};

//它把所有的监听器运行一次,返回一个布尔值,表示是否还有变更了
Scope.prototype.$$digestOnce = function() {
  var self  = this;
  var dirty;
  _.forEach(this.$$watchers, function(watch) {
    try {
      var newValue = watch.watchFn(self);
      var oldValue = watch.last;
      if (!self.$$areEqual(newValue, oldValue, watch.valueEq)) {
        watch.listenerFn(newValue, oldValue, self);
        dirty = true;
      }
      watch.last = (watch.valueEq ? _.cloneDeep(newValue) : newValue);
    } catch (e) {
      (console.error || console.log)(e);
    }
  });
  return dirty;
};

Scope.prototype.$digest = function() {
  var ttl = 10;
  var dirty;
  this.$beginPhase("$digest");
  
  // 脏检查(dirty check )
  do {
    while (this.$$asyncQueue.length) {
      try {
        var asyncTask = this.$$asyncQueue.shift();
        this.$eval(asyncTask.expression);
      } catch (e) {
        (console.error || console.log)(e);
      }
    }
    dirty = this.$$digestOnce();
    if (dirty && !(ttl--)) {
      this.$clearPhase();
      throw "10 digest iterations reached";
    }
  } while (dirty);
  this.$clearPhase();

  while (this.$$postDigestQueue.length) {
    try {
      this.$$postDigestQueue.shift()();
    } catch (e) {
      (console.error || console.log)(e);
    }
  }
};

Scope.prototype.$eval = function(expr, locals) {
  return expr(this, locals);
};

Scope.prototype.$apply = function(expr) {
  try {
    this.$beginPhase("$apply");
    return this.$eval(expr);
  } finally {
    this.$clearPhase();
    this.$digest();
  }
};

Scope.prototype.$evalAsync = function(expr) {
  var self = this;
  if (!self.$$phase && !self.$$asyncQueue.length) {
    setTimeout(function() {
      if (self.$$asyncQueue.length) {
        self.$digest();
      }
    }, 0);
  }
  self.$$asyncQueue.push({scope: self, expression: expr});
};

Scope.prototype.$$postDigest = function(fn) {
  this.$$postDigestQueue.push(fn);
};

测试一下

var scope = new Scope();
scope.aValue = "abc";
scope.counter = 0;

var removeWatch = scope.$watch(
  function(scope) {
    return scope.aValue;
  },
  function(newValue, oldValue, scope) {
    scope.counter++;
  }
);

scope.$digest();
console.assert(scope.counter === 1);

scope.aValue = 'def';
scope.$digest();
console.assert(scope.counter === 2);

removeWatch();
scope.aValue = 'ghi';
scope.$digest();
console.assert(scope.counter === 2); // No longer incrementing

angularjs的脏检查,每次数据改变都会检查是否需要重新绑定,有很大的性能问题。 vue和angular都对此进行了重写。

参考。www.ituring.com.cn/article/398…