【Vue源码】17.响应式原理-计算属性

响应式原理-计算属性

计算属性的初始化是定义在 initState 函数中,执行了 if (opts.computed) initComputed(vm, opts.computed)initComputed 的定义在 src/core/instance/state.js 中:

function initComputed(vm: Component, computed: Object) {
  // 注意这个 vm._computedWatchers 对象,后面的 createComputedGetter 方法中要用到了
  // $flow-disable-line
  const watchers = (vm._computedWatchers = Object.create(null));
  // computed properties are just getters during SSR
  const isSSR = isServerRendering();

  for (const key in computed) {
    const userDef = computed[key];
    const getter = typeof userDef === "function" ? userDef : userDef.get;
    if (process.env.NODE_ENV !== "production" && getter == null) {
      warn(`Getter is missing for computed property "${key}".`, vm);
    }

    if (!isSSR) {
      // create internal watcher for the computed property.
      // 为每一个key创建Watcher实例
      // 此时 watcher 的 getter 为 fullName () { return this.firstName + ' ' + this.lastName },但是不会立即求值,并且实例化一个 Dep
      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)) {
      // 将每个key代理到 vm 上
      defineComputed(vm, key, userDef);
    } else if (process.env.NODE_ENV !== "production") {
      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
        );
      }
    }
  }
}
复制代码

initComputed 做了下面几件事:

  1. 定义 watchersvm._computedWatchers
  2. 遍历 computed
  • 拿到用户自定义的 getter,并为之创建 computed watcher
  • 如果 !key in vm 调用 defineComputed,利用 Object.defineProperty 给计算属性的 key 添加 gettersetter

defineComputed 定义在 src/core/instance/state.js 中:

export function defineComputed(
  target: any,
  key: string,
  userDef: Object | Function
) {
  const shouldCache = !isServerRendering();
  if (typeof userDef === "function") {
    sharedPropertyDefinition.get = shouldCache
      ? createComputedGetter(key)
      : userDef;
    sharedPropertyDefinition.set = noop;
  } else {
    sharedPropertyDefinition.get = userDef.get
      ? shouldCache && userDef.cache !== false
        ? createComputedGetter(key)
        : userDef.get
      : noop;
    sharedPropertyDefinition.set = userDef.set ? userDef.set : noop;
  }
  if (
    process.env.NODE_ENV !== "production" &&
    sharedPropertyDefinition.set === noop
  ) {
    sharedPropertyDefinition.set = function () {
      warn(
        `Computed property "${key}" was assigned to but it has no setter.`,
        this
      );
    };
  }
  Object.defineProperty(target, key, sharedPropertyDefinition);
}
复制代码

createComputedGetter 定义在 src/core/instance/state.js 中:

function createComputedGetter(key) {
  return function computedGetter() {
    // this._computedWatchers 是在上面 initComputed 的时候添加到 vm 实例上的
    const watcher = this._computedWatchers && this._computedWatchers[key];
    if (watcher) {
      watcher.depend();
      // 调用 watcher.evaluate 就是求值,求 watcher 接收到的 expOrFn 的值
      return watcher.evaluate();
    }
  };
}
复制代码

这就是 computed 的初始化做的事情,下面我们通过一个例子来看一下 computed 的获取和渲染过程。

<template>
  <div id="app">
    {{ fullName }}
  </div>
</template>

<script>
export default {
  name: "App",
  data() {
    return {
      firstName: "SHI",
      lastName: "QI",
    };
  },
  computed: {
    fullName() {
      return this.firstName + this.lastName;
    },
  },
  methods: {
    changeLast() {
      this.lastName = "QILONG";
    },
  },
};
</script>
复制代码

当我们的 render 函数访问到 this.fullName 时,会触发计算属性的 getter,即为:

function createComputedGetter(key) {
  return function computedGetter() {
    // this._computedWatchers 是在上面 initComputed 的时候添加到 vm 实例上的
    const watcher = this._computedWatchers && this._computedWatchers[key];
    if (watcher) {
      watcher.depend();
      // 调用 watcher.evaluate 就是求值,求 watcher 接收到的 expOrFn 的值
      return watcher.evaluate();
    }
  };
}
复制代码

他会拿到计算属性的 watcher,执行 watcher.depend

/**
 * Depend on this watcher. Only for computed property watchers.
 * 仅适用于 computed watchers
 * 此时 Dep.target 是渲染 Watcher
 */
depend () {
  if (this.dep && Dep.target) {
    this.dep.depend()
  }
}
复制代码

注意,此时 Dep.target 是渲染 Watcher,所以 this.dep.depend() 相当于渲染 Watcher 订阅了这个 computed watcher 的变化。
然后执行 watcher.evaluate() 求值:

/**
 * Evaluate and return the value of the watcher.
 * This only gets called for computed property watchers.
 */
evaluate () {
  if (this.dirty) {
    this.value = this.get()
    this.dirty = false
  }
  return this.value
}
复制代码

evaluate 的逻辑非常简单,判断 this.dirty,如果为 true 则通过 this.get() 求值,然后把 this.dirty 设置为 false。在求值过程中,会执行 value = this.getter.call(vm, vm),这实际上就是执行了计算属性定义的 getter 函数,在我们这个例子就是执行了 return this.firstName + ' ' + this.lastName

在这个过程中,因为 firstNamelastName 都是响应式数据,所以会触发他们的 getter,根据我们之前的分析,他会把自身持有的 dep 添加到目前正在计算的 watcher 中,这个时候 Dep.target 就是 computed watcher

最后通过 return this.value 返回最终值。我们目前知道了计算属性的求值过程,接下来看它依赖的数据变化之后的逻辑。

当我们修改 firstName 时,会触发他的 setter 方法,通知所有订阅它变化的 watcher 更新,执行 watcher.update() 方法:

/* istanbul ignore else */
if (this.computed) {
  // 懒执行时走这里,比如 computed
  // 将 dirty 置为 true,就可以让 computedGetter 执行时重新计算 computed 回调函数的执行结果

  // A computed property watcher has two modes: lazy and activated.
  // It initializes as lazy by default, and only becomes activated when
  // it is depended on by at least one subscriber, which is typically
  // another computed property or a component's render function.
  if (this.dep.subs.length === 0) {
    // In lazy mode, we don't want to perform computations until necessary,
    // so we simply mark the watcher as dirty. The actual computation is
    // performed just-in-time in this.evaluate() when the computed property
    // is accessed.
    this.dirty = true;
  } else {
    // In activated mode, we want to proactively perform the computation
    // but only notify our subscribers when the value has indeed changed.
    this.getAndInvoke(() => {
      this.dep.notify();
    });
  }
} else if (this.sync) {
  // 在使用 $watch 或者 watch 选项时,可以传入一个 sync 选项,标识 watcher 需要同步更新
  this.run();
} else {
  // 一般的 watcher 更新都是异步队列,将 watcher 放入到更新对象队列
  queueWatcher(this);
}
复制代码

对于计算属性,有两种模式 lazyactive。如果 this.dep.subs.length === 0 成立,则说明没有人去订阅这个 computed watcher 的变化,仅仅把 dirty 设置为 true,只有当再次访问的时候才会重新求值。在我们的场景下,渲染 watcher 订阅了这个 computed watcher 的变化,所以他会执行:

this.getAndInvoke(() => {
  this.dep.notify()
})

getAndInvoke (cb: Function) {
  const value = this.get()
  if (
    value !== this.value ||
    // Deep watchers and watchers on Object/Arrays should fire even
    // when the value is the same, because the value may
    // have mutated.
    isObject(value) ||
    this.deep
  ) {
    // set new value
    const oldValue = this.value
    this.value = value
    this.dirty = false
    if (this.user) {
      try {
        cb.call(this.vm, value, oldValue)
      } catch (e) {
        handleError(e, this.vm, `callback for watcher "${this.expression}"`)
      }
    } else {
      cb.call(this.vm, value, oldValue)
    }
  }
}
复制代码

getAndInvoke 会重新计算,对比新旧值(再次计算的时候,因为之前已经添加过依赖,所以在 watcher.depIds 中已经缓存)。如果新旧值不一致则执行回调函数,那么这里这个回调函数是 this.dep.notify(),在我们这个场景下就是触发了渲染 watcher 重新渲染。

总结

17.响应式原理-计算属性.png
通过以上的分析,我们知道计算属性本质上就是一个 computed watcher,也了解了它的创建过程和被访问触发 getter 以及依赖更新的过程,其实这是最新的计算属性的实现,之所以这么设计是因为 Vue 想确保不仅仅是计算属性依赖的值发生变化,而是当计算属性最终计算的值发生变化才会触发渲染 watcher 重新渲染,本质上是一种优化。

源码分析 GitHub 地址

参考:Vue.js 技术揭秘

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享