vue2关于computed初始化源码解析

289 阅读5分钟

1、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);
      } else if (vm.$options.methods && key in vm.$options.methods) {
        warn(("The computed property \"" + key + "\" is already defined as a method."), vm);
      }
    }
  }
}
  1. _computedWatchers存储计算属性的watcher(观察者)。
  2. 首先判断是否在服务端渲染环境中,为什么要获取这个?这里稍后讲解。
  3. 遍历 每个计算属性,并获取计算属性的userDef,userDef可能是一个函数或者对象,定义变量为getter。
  4. 判断如果不在在服务端渲染环境中,就创建watcher,并存储在_computedWatchers中,之前获取的getter是为了在watcher获取计算属性值;否则就不创建。由于watcher可以实现vue的响应式,而如果是服务器环境下,数据和模板从后端获取即可而不需要在前端收集依赖,不需要缓存机制,因此在服务器并不需要这一点,这就是对2的解答。当然在后面的defineComputed方法会更容易明白。
  5. 如果计算属性不在vue实例中,就定义。

2、defineComputed源码解析

var sharedPropertyDefinition = {
  enumerable: true,
  configurable: true,
  get: noop,
  set: noop
}
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
    }
  }
}
  1. sharedPropertyDefinition是一个全局的对象,用于对象绑定属性的get set方法代理。
  2. 定义缓存标识shuldCache,其实就是判断是否在服务端渲染环境中。
  3. 首先也要判断userDef是否是一个函数。
    1. 如果是一个函数,且不在服务端环境下,调用该方法createComputedGetter创建计算属性的get方法;如果在服务端环境下,执行createGetterInvoker。
    2. 如果userDef是对象,就获取该对象的get和set
    3. 如果最后获取的set不合法,那就在设置计算属性值的时候报错就行了。

3、createComputedGetter源码解析

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
    }
  }
}
function createGetterInvoker(fn) {
  return function computedGetter() {
    return fn.call(this, this)
  }
}
  1. createComputedGetter是一个高阶函数,返回一个函数,watcher的dirty决定是否缓存;createGetterInvoker每次都会执行计算属性的函数,没有缓存的功能。
  2. 当获取计算属性的时候,就会执行computedGetter方法,这就是计算属性的get方法。
  3. 可以想象一下,当渲染函数解析到某个计算属性的时候,就会执行computedGetter方法,过程如下:
    1. 首先获取计算属性的watcher
    2. 如果watcher.dirty为真,执行watcher.evaluate,这个方法源码如下:
Watcher.prototype.evaluate = function evaluate() {
  this.value = this.get();
  this.dirty = false;
}

该方法执行watcher的get方法,这个方法不详述了,目的就是获取计算属性的值,其次是可以把计算属性的userDef收集到依赖属性的依赖中。最后再把dirty设置为false。这里的dirty就是是否缓存的判断。比如计算属性A依赖数据B,那么这段代码执行就可以给B收集依赖C,而C就是计算属性的watcher。当渲染函数读取到B且B值变化时,就会触发B依赖的update,也就是计算属性watcher的update,

Watcher.prototype.update = function update() {
  /* istanbul ignore else */
  if (this.lazy) {
    this.dirty = true;
  } else if (this.sync) {
    this.run();
  } else {
    queueWatcher(this);
  }
}

当执行update时,会把dirty设置为true。同时B还有渲染页面的依赖,所以会再次执行页面渲染,当渲染函数读取计算属性时,就会执行watcher.evaluate()获取到计算属性最新的值了。所以如果B值没有变化时,也就是A不会变化,此时就不会触发B依赖的update,也就不会更新dirty,也就不会执行watcher.evaluate(),这就是缓存机制。

  1. 接下来的这段源码可能有点迷惑
 if (Dep.target) {
        watcher.depend();
 }

Dep.target是当前环境中需要收集的watcher。 watcher.depend(),是这样执行的

Watcher.prototype.depend = function depend() {
  var i = this.deps.length;
  while (i--) {
    this.deps[i].depend();
  }
}

因为watcher中都有依赖的存储,如deps就是与该watcher相关的依赖。所以上面代码的意思是watcher中所有依赖依次收集Dep.target,因为收集依赖时,watcher的addDeps方法会有判断,无需担心重复收集依赖。 而上面那段代码的意思就是计算属性watcher的所有依赖依次收集Dep.target。 为什么这么做呢?举个例子。

  1. 比如A和B都是计算属性,B依赖于C,A依赖于B,
    1. 当读取A时,会首先把A的watcher赋值给Dep.target并pushTarget,
    2. 接下来会读取B,而B的watcher恢复至Dep.target且pushTarget
    3. 接下来读取C,C收集到B的watcher
    4. 然后执行popTarget,弹出B的watcher,Dep.target重新赋值等于A的watcher
    5. 然而此时,watcher.evaluate已经执行完毕。
    6. 如果这样结束,会存在问题,那就是,当C更改时,只会更新B的dirty获取B的值;那么A呢?
    7. 所以需要C的依赖收集A的watcher,
    8. 而且此时,也就是执行完B的watcher.evaluate后,Dep.target正好等于A的watcher,所以B的watcher.depend(),就可以把C的依赖把A的watcher收集完成。
    9. 当C变化时,就会触发A的dirty且会获取A的值了。
  2. 还有一种情况,就是模板中只用到了计算属性,计算属性的依赖数据没有出现在模板中。
    1. 那么执行这段代码就会把渲染模板的依赖收集到依赖数据的依赖中
    2. 因为计算属性的值是通过渲染模板才能触发的,所以依赖数据中需要有渲染模板的watcher
  3. 应该还有其他情况。不得不说 这段思想太巧妙了。