通俗易懂的 Vue - Computed 原理(Watcher and Dep)

大家好,新人一个,初次写博客还请大家多多关照。

对于Vue的响应式,想必大家都有所了解,在Vue响应式数据中,computed 是比较特殊的响应式数据,它们可以监听使用到的 数据,数据 改变 computed 的数据也会重新计算。

今天主要是讨论 computed 实现原理 。 computed 在内部主要是运用 Watcher 和 Dep 构造函数进行收集依赖和派发更新。

咱们先来看看 Watcher 和 Dep 源码。

  var uid = 0;

  /**
   * dep 就是用来给每个数据做个标记,可以用来收集数据和派发更新
   */
  var Dep = function Dep () {
    this.id = uid++; 
//给每一个 Dep 打一个标记。这里需要说明一下,vm中data的每个数据进行初始化的时候都会调用this.dep = new Dep(),每一个数据都会有dep属性。
//(通过Observer 构造函数进行数据初始化,这里就不多说了,大家感兴趣可以看一下源码)
    this.subs = []; // 这个subs收集的是watcher,派发更新的时候遍历每个watcher调用update方法
  };

  Dep.prototype.addSub = function addSub (sub) {
    this.subs.push(sub);//这个方式是用来收集watcher的(每个computed构建出来的Watcher实例)
  };

  Dep.prototype.depend = function depend () {
    if (Dep.target) {
      Dep.target.addDep(this);  // 这里的方法是Watcher原型上的addDep方法,请看下面
    }
  };

  Watcher.prototype.addDep = function addDep (dep) {
    var id = dep.id;
    if (!this.depIds.has(id)) {
      this.depIds.add(id); //将依赖的每个dep,添加到 watcher 的 deps集合中,完成数据的收集
      this.depIds.push(dep);
    }
  };

  Dep.prototype.notify = function notify () { 
// 用来出发watcher的更新,每当数据改变,每个数据的dep就会出发motify方法,将收集的watcher逐个进行数据更新,
//这里就是为什么computed知道依赖改变了,然后就会自动更新。其实不是computed知道依赖改变的,而是依赖改变以后出发computed更新。
    for (var i = 0, l = subs.length; i < l; i++) {
      subs[i].update();
    }
  };

  Dep.target = null;
  var targetStack = [];

  function pushTarget (target) {
    targetStack.push(target); // 将 Dep 原型 上的 Target 设置为 watcher
    Dep.target = target;
  }

  function popTarget () {
    targetStack.pop();
    Dep.target = targetStack[targetStack.length - 1];
  }
  // vm 是 Vue 实例
  // expOrFn 是传过来的数据 get 函数
  // cb 是回调函数

  var Watcher = function Watcher (vm,expOrFn,cb,) {
    this.vm = vm;
    this.cb = cb;
    this.id = ++uid$2; // uid for batching
    this.deps = []; // deps 是用来收集  依赖数据节点  的集合
    this.depIds = new _Set();
    this.value = this.get();  // 首次执行get方法(get也就是expOrFn),初次收集依赖是在这个环节
  }; 


  Watcher.prototype.get = function get () {
    pushTarget(this); // 将 Dep 的 target 设置为当前 watcher,这个函数在 Dep 那块最后面
    var value;
    var vm = this.vm;
    value = this.getter.call(vm, vm);
    popTarget();
    this.cleanupDeps(); 将 Dep 的 target 设置为当前 空,这个函数我会放在后面
    }
    return value
  };

  /**
   * 添加依赖的方法,重点!!! 着重看一下 (我进行了一些简化,便于理解)
   */
  Watcher.prototype.addDep = function addDep (dep) {
    var id = dep.id;
    if (!this.depIds.has(id)) {
      this.depIds.add(id);
      this.depIds.push(dep);
    }
    if (!this.depIds.has(id)) {
        dep.addSub(this);
      } //这步是将自身(也就是watcher)添加到dep中的watcher集合,派发数据更新时用到
  };

  /**
   * 更新数据的方法,在派发更新的时候会用到。 computed 更新数据的时候,用 dep 的 notify 方法进 
   * 行更新数据,更新数据主要调用的是 run 方法
   */
  Watcher.prototype.update = function update () {
    /* istanbul ignore else */
    if (this.lazy) {
      this.dirty = true;
    } else if (this.sync) {
      this.run();
    } else {
      queueWatcher(this);
    }
  };

  /**
   * 在这个阶段主要是运行get方法,拿到数据 (简化以后的代码)
   */
  Watcher.prototype.run = function run () {
    if (this.active) {
      var value = this.get();
      this.value = value 
    }
  };

  /**
   * 深度收集依赖,computed 可以收集 computed 数据就是依靠这个方法
   */
  Watcher.prototype.depend = function depend () {
    console.log(this.deps)
    var i = this.deps.length;
    while (i--) {
      this.deps[i].depend(); //注意这里的 depend 方法是 Dep 原型上的方法,不是Watcher 的法
    }
  };

大家可以仔细看一下我的注释,进行了每个步骤的描述,当然都只是我自己的理解,有不对的地方还请大家多多指教。

现在来细说一下收集依赖的流程。当初始化Vue实例以后, 在初始化 computed 阶段,vue 会对每个 computed 进行运算和收集依赖。当 computed 初始化的时候会依靠当前的 computed 生成一个 Watcher,并且将 getter 方法传入。

function initComputed (vm, computed) {
    var watchers = vm._computedWatchers = Object.create(null);
    for (var key in computed) {
      var userDef = computed[key];
      var getter = typeof userDef === 'function' ? userDef : userDef.get;
      if (getter == null) {
        warn(
          ("Getter is missing for computed property \"" + key + "\"."),
          vm
        );
      }
        watchers[key] = new Watcher(
          vm,
          getter || noop,
          noop,
          computedWatcherOptions
        );
      }
    )
}

 这时候已经有了watcher实例。 当 watcher 初始化,会调用 get方法(收集依赖在这里),

Watcher.prototype.get = function get () {
    pushTarget(this); // 将 Dep 的 target 设置为当前 watcher,这个函数在 Dep 那块最后面
    var value;
    var vm = this.vm;
    value = this.getter.call(vm, vm);
    popTarget();
    this.cleanupDeps(); 将 Dep 的 target 设置为当前 空,这个函数我会放在后面
    }
    return value
  };

get执行得时候会调用pushTarget方法(这个方法就是将当前的全局的 Dep.target 设置成当前运行的 watcher ),然后会进行 getter(),getter() 执行的时候会触发 每个数据 的 get 方法,请看get源码

Object.defineProperty(obj, key, {
      enumerable: true,
      configurable: true,
      get: function reactiveGetter () {
        var value = getter ? getter.call(obj) : val;
        if (Dep.target) { //判断当前执行是否为watcher,也可以说是否为computed运算的数据
          dep.depend(); // 收集到watcher里面的deps里面
          if (childOb) {
            childOb.dep.depend();
            if (Array.isArray(value)) {
              dependArray(value);
            }
          }
        }
        return value
      })

这里的 get 方法会首先判断是否有 Dep.target,这个判断最终目的是判断当前获取数据的是否是watcher,如果是,就会调用 Dep 的 depend 方法进行收集,这样 watcher 就会把运算的每个数据的dep收集到 自己的 deps 属性中,完成了数据依赖收集。

咱们再来细说一下Dep 的 depend 方法,这个方法主要是调用的 Watcher 里面的 addDep方法, 这个方法进行的是双向数据收集,也就是说 watcher 会收集 deps, 同样 dep 也会收集 watcher 集合,存到dep 的 subs 属性中, 每当 dep 的数据更新时,就会将subs的每个 watcher 进行 update ,这样就完成了数据更新,这就是 computed 的实现原理。

简单点说,就是 computed 在运行的时候,首先将全局的 Dep.target 设置当前 computed 的 watcher,然后在运行 computed 代码,里面用到的数据会调用它们自己的 get 方法,在 get 方法里,他们会将自己的 dep.id 存到当前的 Dep.target 里,然后还要将当前的 Dep.target(也就是当前的 watcher) 存到自己的 dep.subs 属性 中,每当自己数据更新触发 set 方法,就会把自己 dep.subs 中的每个watcher 拿出来进行数据更新,从而更新 computed 。

猜你喜欢

转载自blog.csdn.net/zjk325698/article/details/112226422