# computed计算属性

computed特性是用来处理data, 并且返回最终的data, 最主要的是它具有lazy, 也就是说是惰性函数

# 抛出疑问

  • computed是怎么去准确定位到函数内部依赖的变量, 如何得知依赖的变量已经更新从而触发computed更新?

本章节从源码的角度去感受下computed内部是怎么实现的

# initComputed

  function initComputed (vm, computed) {
    // $flow-disable-line
    var watchers = vm._computedWatchers = Object.create(null);
    // computed properties are just getters during SSR
    var isSSR = isServerRendering();

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

      if (!isSSR) {
        // create internal watcher for the computed property.
        watchers[key] = new Watcher(
          vm,
          getter || noop,
          noop,
          computedWatcherOptions
        );
      }


      // component-defined computed properties are already defined on the
      // component prototype. We only need to define computed properties defined
      // at instantiation here.
      if (!(key in vm)) {
        defineComputed(vm, key, userDef);
      } else {
        if (key in vm.$data) {
          warn(("The computed property \"" + key + "\" is already defined in data."), vm);
        } else if (vm.$options.props && key in vm.$options.props) {
          warn(("The computed property \"" + key + "\" is already defined as a prop."), vm);
        }
      }
    }
  }

我们可以看到vm实例下定义了一个_computedWatchers专门用来收集该实例下computed的依赖函数, 在不是服务端渲染清空下, computed每个属性都会实例化一个Watcher, 依赖函数就是computed函数, computedWatcher我们可以看到它与renderWatcher的区别在于它具有lazy选项, 我们复习下Watcher的实例化时做了什么?

  var Watcher = function Watcher (
    vm,
    expOrFn,
    cb,
    options,
    isRenderWatcher
  ) {
    this.vm = vm;
    if (isRenderWatcher) {
      vm._watcher = this;
    }
    vm._watchers.push(this);
    // options
    if (options) {
      this.deep = !!options.deep;
      this.user = !!options.user;
      this.lazy = !!options.lazy;
      this.sync = !!options.sync;
      this.before = options.before;
    } else {
      this.deep = this.user = this.lazy = this.sync = false;
    }
    this.cb = cb;
    this.id = ++uid$2; // uid for batching
    this.active = true;
    this.dirty = this.lazy; // for lazy watchers
    this.deps = [];
    this.newDeps = [];
    this.depIds = new _Set();
    this.newDepIds = new _Set();
    this.expression = expOrFn.toString();
    // parse expression for getter
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn;
    } else {
      this.getter = parsePath(expOrFn);
      if (!this.getter) {
        this.getter = noop;
        warn(
          "Failed watching path: \"" + expOrFn + "\" " +
          'Watcher only accepts simple dot-delimited paths. ' +
          'For full control, use a function instead.',
          vm
        );
      }
    }
    this.value = this.lazy
      ? undefined
      : this.get();
  };

我们可以看到lazy属性的存在会使初始化时依赖函数不立即执行, 也就是说computed在初始化时并不会去执行对应的get函数, 回过头来我们再看initComputed, 之后会调用defineComputed函数, 该函数就是computed的核心函数

# defineComputed

  function defineComputed (
    target,
    key,
    userDef
  ) {
    var shouldCache = !isServerRendering();
    if (typeof userDef === 'function') {
      sharedPropertyDefinition.get = shouldCache
        ? createComputedGetter(key)
        : createGetterInvoker(userDef);
      sharedPropertyDefinition.set = noop;
    } else {
      sharedPropertyDefinition.get = userDef.get
        ? shouldCache && userDef.cache !== false
          ? createComputedGetter(key)
          : createGetterInvoker(userDef.get)
        : noop;
      sharedPropertyDefinition.set = userDef.set || noop;
    }
    if (sharedPropertyDefinition.set === noop) {
      sharedPropertyDefinition.set = function () {
        warn(
          ("Computed property \"" + key + "\" was assigned to but it has no setter."),
          this
        );
      };
    }
    Object.defineProperty(target, key, sharedPropertyDefinition);
  }

  function createComputedGetter (key) {
    return function computedGetter () {
      var watcher = this._computedWatchers && this._computedWatchers[key];
      if (watcher) {
        if (watcher.dirty) {
          watcher.evaluate();
        }
        if (Dep.target) {
          watcher.depend();
        }
        return watcher.value
      }
    }
  }

我们可以看到Vue将对应的computed代理到实例下, 我们具体看一下createComputedGetter函数, 该函数是用来收集依赖函数的

重新回到渲染函数, 我们在渲染template的时候会去获取对应的computed, 从而就会触发createComputedGetter函数, 随后我们剖析下内部究竟做了什么?

computed如何使内部依赖变量收集到computedWatcher

_computedWatchers我们之前说到了是用来收集实例下所有的computedWatcher, 找到对应的computedWatcher, 此时我们在之前说到了computedWatcher会传入lazy选项用于惰性处理, lazy会被赋值给dirty变量, 此时computedWatcher会执行watcher.evaluate()

  /**
   * Evaluate the value of the watcher.
   * This only gets called for lazy watchers.
   */
  Watcher.prototype.evaluate = function evaluate () {
    this.value = this.get();
    this.dirty = false;
  };

evaluate会执行watcher的依赖函数, 此时会触发computed依赖的变量从而触发对应变量的get函数, 变量的dep就会收集到该computedWatcher, 执行完后并将dirty变为false(这一步就是为之后的lazy做处理) 随后Dep.target存在的情况下会执行watcher.depend函数(注意: Dep.target此时是渲染Watcher)

  /**
   * Depend on all deps collected by this watcher.
   */
  Watcher.prototype.depend = function depend () {
    var i = this.deps.length;
    while (i--) {
      this.deps[i].depend();
    }
  }

我们知道deps就是拥有该Watcher的所有dep

  Dep.prototype.depend = function depend () {
    if (Dep.target) {
      Dep.target.addDep(this);
    }
  };

这里可能有点绕, 我们来理一下, computedWatcher收集到的dep(也就是computed依赖的变量的dep集合)调用depend函数, 此时的Dep.target是渲染Watcher, depend里的this是指的是computedWatcher收集到的dep, 渲染Watcher会去添加该变量的dep(因为该变量可能没有出现在template里, 所以需要去收集该变量的dep, Watcher内部本身就有去重处理), 随后返回了computed执行后的值, 这里我们就可以看到computed从创建到收集依赖以及最后的派发更新的整个过程

# 总结

computed在初始化的时候会执行一次get函数, 随后就是由依赖的变量的更新触发computedWatcher的执行, 从而引起computed的更改, 这也是computed的魅力所在, 下一节watch的原理剖析

前端马丁 关注我, 不迷路