7.10 computed

计算属性设计的初衷是用于简单运算的,毕竟在模板中放入太多的逻辑会让模板过重且难以维护。在分析computed时,我们依旧遵循依赖收集和派发更新两个过程进行分析。

7.10.1 依赖收集

computed的初始化过程,会遍历computed的每一个属性值,并为每一个属性实例化一个computed watcher,其中{ lazy: true}computed watcher的标志,最终会调用defineComputed将数据设置为响应式数据,对应源码如下:

  1. function initComputed() {
  2. ···
  3. for(var key in computed) {
  4. watchers[key] = new Watcher(
  5. vm,
  6. getter || noop,
  7. noop,
  8. computedWatcherOptions
  9. );
  10. }
  11. if (!(key in vm)) {
  12. defineComputed(vm, key, userDef);
  13. }
  14. }
  15. // computed watcher的标志,lazy属性为true
  16. var computedWatcherOptions = { lazy: true };

defineComputed的逻辑和分析data的逻辑相似,最终调用Object.defineProperty进行数据拦截。具体的定义如下:

  1. function defineComputed (target,key,userDef) {
  2. // 非服务端渲染会对getter进行缓存
  3. var shouldCache = !isServerRendering();
  4. if (typeof userDef === 'function') {
  5. //
  6. sharedPropertyDefinition.get = shouldCache
  7. ? createComputedGetter(key)
  8. : createGetterInvoker(userDef);
  9. sharedPropertyDefinition.set = noop;
  10. } else {
  11. sharedPropertyDefinition.get = userDef.get
  12. ? shouldCache && userDef.cache !== false
  13. ? createComputedGetter(key)
  14. : createGetterInvoker(userDef.get)
  15. : noop;
  16. sharedPropertyDefinition.set = userDef.set || noop;
  17. }
  18. if (sharedPropertyDefinition.set === noop) {
  19. sharedPropertyDefinition.set = function () {
  20. warn(
  21. ("Computed property \"" + key + "\" was assigned to but it has no setter."),
  22. this
  23. );
  24. };
  25. }
  26. Object.defineProperty(target, key, sharedPropertyDefinition);
  27. }

在非服务端渲染的情形,计算属性的计算结果会被缓存,缓存的意义在于,只有在相关响应式数据发生变化时,computed才会重新求值,其余情况多次访问计算属性的值都会返回之前计算的结果,这就是缓存的优化computed属性有两种写法,一种是函数,另一种是对象,其中对象的写法需要提供gettersetter方法。

当访问到computed属性时,会触发getter方法进行依赖收集,看看createComputedGetter的实现。

  1. function createComputedGetter (key) {
  2. return function computedGetter () {
  3. var watcher = this._computedWatchers && this._computedWatchers[key];
  4. if (watcher) {
  5. if (watcher.dirty) {
  6. watcher.evaluate();
  7. }
  8. if (Dep.target) {
  9. watcher.depend();
  10. }
  11. return watcher.value
  12. }
  13. }
  14. }

createComputedGetter返回的函数在执行过程中会先拿到属性的computed watcher,dirty是标志是否已经执行过计算结果,如果执行过则不会执行watcher.evaluate重复计算,这也是缓存的原理。

  1. Watcher.prototype.evaluate = function evaluate () {
  2. // 对于计算属性而言 evaluate的作用是执行计算回调
  3. this.value = this.get();
  4. this.dirty = false;
  5. };

get方法前面介绍过,会调用实例化watcher时传递的执行函数,在computer watcher的场景下,执行函数是计算属性的计算函数,他可以是一个函数,也可以是对象的getter方法。

列举一个场景避免和data的处理脱节,computed在计算阶段,如果访问到data数据的属性值,会触发data数据的getter方法进行依赖收集,根据前面分析,dataDep收集器会将当前watcher作为依赖进行收集,而这个watcher就是computed watcher,并且会为当前的watcher添加访问的数据Dep

回到计算执行函数的this.get()方法,getter执行完成后同样会进行依赖的清除,原理和目的参考data阶段的分析。get执行完毕后会进入watcher.depend进行依赖的收集。收集过程和data一致,将当前的computed watcher作为依赖收集到数据的依赖收集器Dep中。

这就是computed依赖收集的完整过程,对比data的依赖收集,computed会对运算的结果进行缓存,避免重复执行运算过程。

7.10.2 派发更新

派发更新的条件是data中数据发生改变,所以大部分的逻辑和分析data时一致,我们做一个总结。

  • 当计算属性依赖的数据发生更新时,由于数据的Dep收集过computed watch这个依赖,所以会调用depnotify方法,对依赖进行状态更新。
  • 此时computed watcher和之前介绍的watcher不同,它不会立刻执行依赖的更新操作,而是通过一个dirty进行标记。我们再回头看依赖更新的代码。
  1. Dep.prototype.notify = function() {
  2. ···
  3. for (var i = 0, l = subs.length; i < l; i++) {
  4. subs[i].update();
  5. }
  6. }
  7. Watcher.prototype.update = function update () {
  8. // 计算属性分支
  9. if (this.lazy) {
  10. this.dirty = true;
  11. } else if (this.sync) {
  12. this.run();
  13. } else {
  14. queueWatcher(this);
  15. }
  16. };

由于lazy属性的存在,update过程不会执行状态更新的操作,只会将dirty标记为true

  • 由于data数据拥有渲染watcher这个依赖,所以同时会执行updateComponent进行视图重新渲染,而render过程中会访问到计算属性,此时由于this.dirty值为true,又会对计算属性重新求值。