前言

不同于data的数据拦截,计算属性computed稍微是复杂的。计算属性computed存在缓存效果,针对于计算属性的提出以下3个问题点:

  • Vue对于computed的具体处理逻辑
  • computed的缓存是如何实现的
  • computed是如何收集依赖的

Vue对于computed的具体处理逻辑

计算属性computed属于Vue实例的选项,其处理逻辑在源码中都是从initState中开始的。

if (opts.computed) {
	initComputed(vm, opts.computed);
}

initComputed的具体逻辑如下:
在这里插入图片描述
从上面逻辑可知如下几点:

  • 计算属性选项会独立存在一个Watcher对象集合中心_computedWatchers
  • computed选项下每一个属性都会对应一个watcher实例
  • 不同于props和data的数据拦截处理,computed是调用defineComputed来实现响应式
computed中watcher对象生成

这部分源码逻辑如下:

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

这里需要注意的是computedWatcherOptions,该对象只存在一个lazy属性即:

var computedWatcherOptions = { lazy: true };

lazy属性比较关键,其作用就是延迟watcher对象的getter函数执行。

var Watcher = function Watcher (
  vm,
  expOrFn,
  cb,
  options,
  isRenderWatcher
) {
  // options
  if (options) {
    this.lazy = !!options.lazy;
  } 
  this.value = this.lazy
    ? undefined
    : this.get();
};

从上面逻辑可知当lazy属性为true,就不会立即执行get方法。

defineComputed

具体看看defineComputed的处理逻辑:
这里写图片描述

从上面逻辑可知,defineComputed的逻辑分为2点:

  • 针对浏览器环境需要构建特殊处理getter部分,即使用createComputedGetter来构建getter
  • 使用Object.defineProperty来实现数据拦截

computed缓存和依赖收集

计算属性computed数据拦截的getter是通过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
    }
  }
}

这里主要的逻辑就是:

  • watcher.dirty为true执行实例方法evaluate
  • Dep.target存在前提下执行实例方法depend
evaluate实例方法
Watcher.prototype.evaluate = function evaluate () {
  this.value = this.get();
  this.dirty = false;
};

实际上就是执行watcher的getter函数,对于计算属性就是执行其对应的getter即createComputedGetter生成的computedGetter。

在计算属性的computedGetter函数中,会依据watcher对象的dirty属性判断是否执行evaluate方法,计算属性对应的watcher对象其dirty属性值默认是true,即第一次调用就会执行computedGetter函数。

而evalute方法执行后会将dirty设置为false,dirty属性就是computed控制缓存的关键。其过程如下:

例如getComputed计算属性:

  • 第一次被调用其watcher对象的dirty为true,所以执行evaluate实例方法生成value值,其dirty值会被置为false
  • 第二次被调用,假设其依赖没有任何更新,dirty为false其evaluate就不会被执行,返回的还是上一次的value

上面是计算属性的缓存过程,缓存是通过dirty来控制,如果内部依赖更新那么理论上就需要更新dirty值为true。实际上这部分逻辑就是如此,dirty设置为true的逻辑是watcher对象的实例方法update中即视图更新触发的。

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

计算属性的lazy为true,所以会更新dirty为true,从而会触发生成新的value值。

内部依赖收集

data中属性的依赖关联都是在属性Object.defineProperty数据拦截的getter执行时建立的即Dep对象和Watcher对象的关联。

计算属性内部可能会调用其他被拦截的属性,当使用的内部依赖属性值更新时就会触发计算属性的更新。计算属性的内部依赖的依赖收集是evaluate执行时:

evaluate方法执行就调用计算属性的实际逻辑,这个过程会获取属性,对于已劫持的属性就会触发其getter,属性对应的Dep对象就与该计算属性对应的watcher对象建立联系,计算属性具体逻辑执行完毕就会建立所有内部属性对应的Dep对象与计算属性本身Watcher对象关联

计算属性本身成为依赖
if (Dep.target) {
	watcher.depend();
}

关键在于Dep.target,实际上Vue中比较复杂的也就是Dep.target对象的Watcher对象。实际上Watcher的实例方法depend是将Watcher对象绑定到对应Dep对象中,而Dep的实例方法depend是将Dep绑定到对应的Watcher对象中。

计算属性是特殊的,其本身是变量内部也可以调用其他被拦截的变量,从依赖性质来说:

计算属性存在内部依赖对象(内部依赖的其他被拦截的属性)也存在外部依赖对象(作为其他属性的子依赖)

虽然计算属性可以作为变量使用,例如一个计算属性依赖另外一个计算属性。计算属性的外部依赖只是概念性的,实际上计算属性不存在一个Dep对象,没法与其他watcher对象建立关联,都是与存在对应Dep对象的属性建立关联的:

如果一个计算属性A依赖计算属性B,计算属性B依赖属性c,计算属性不会存在一个与之对应的Dep对象,实际上是c与[A, B]关联

实际上可以通过上面evaluate方法的执行收集依赖的逻辑可以很清晰的知道。

总结

计算属性是特殊的实际也不特殊,特殊点在于:

  • 计算属性存在缓存

  • 每一个计算属性都对应一个watcher对象

    data选项实际上并没有单独对应一个watcher对象,是整体对应一个全局的watcher对象即视图渲染相关的watcher对象

  • 计算属性依赖收集方式是执行整个逻辑从而建立内部所有属性与当前watcher对象的关联,多层计算属性调用最后都会是使用到的属性与其watcher对象建立联系,而不是计算属性与计算属性之间建立联系

实际上也不必纠结多层依赖这样的复杂关系的依赖收集逻辑,只要明确以下2点即可:

  • 无论Dep对象关联多少个Watcher对象还是Watcher对象依赖多少个Dep对象,彼此都会通过相关属性存储以达到关联
  • 依赖收集都是在其被调用时触发即Object.defineProperty中的get函数中处理的
Logo

前往低代码交流专区

更多推荐