根据调试工具看Vue源码之computed(一)

官方定义

  • 类型:{ [key: string]: Function | { get: Function, set: Function } }
  • 详细:计算属性将被混入到 Vue 实例中。全部 getter 和 setter 的 this 上下文自动地绑定为 Vue 实例...
    计算属性的结果会被缓存,除非依赖的响应式属性变化才会从新计算。注意,若是某个依赖 (好比非响应式属性) 在该实例范畴以外,则计算属性是不会被更新的。

上面这几段话其实能够概括为如下几点:javascript

  • computed是计算属性,会被混入到Vue实例中
  • computed的结果会被缓存,除非依赖的响应式属性变化才会从新计算

如何初始化computed

同以往同样,先新建一个Vue项目,同时加入如下代码:前端

export default {
  name: 'test',
  data () {
    return {
      app: 666
    }
  },
  created () {
    console.log('app proxy -->', this.appProxy)
  },
  computed () {
    appProxy () {
      debugger
      return this.app
    }
  }
}

复制代码

F12打开调试界面,刷新后断点停在了debugger的位置,同时能够看到右边的调用栈:java

  • appProxy
  • get
  • evaluate
  • computedGetter
  • created
  • ...

瞥到computedGetter以后,点进去,能够看到:express

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
    }
  }
}
复制代码

看到这里不由一脸懵逼😬
固然,根据前面咱们看源码的经验,没有思路时,直接搜索相关函数的调用位置,这里咱们能够直接搜索createComputedGetter,看看它是在哪里调用的。此处忽略搜索的过程,直接给出个人结论:缓存

Vue中存在两种初始化computed的方法:微信

  1. option中初始化
  2. Vue.prototype.extend函数中初始化

这两种初始化其实大同小异,咱们选择在组件中写computed,天然断点就会跑到Vue.prototype.extend函数里:app

...
if (Sub.options.computed) {
  initComputed$1(Sub);
}
...
复制代码

initComputed$1函数:函数

function initComputed$1 (Comp) {
  // 拿到组件的computed
  var computed = Comp.options.computed;
  for (var key in computed) {
    // 循环遍历
    defineComputed(Comp.prototype, key, computed[key]);
  }
}
复制代码

显然,这句代码:defineComputed(Comp.prototype, key, computed[key])computed挂载在了组件的原型上,下面来看下它的实现方式:oop

defineComputedui

function defineComputed ( target, key, userDef ) {
  // 判断是否要将结果缓存下来
  var shouldCache = !isServerRendering();
  // 下面进行分类判断
  // 对应的computed是函数的状况
  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 (process.env.NODE_ENV !== 'production' &&
      sharedPropertyDefinition.set === noop) {
    sharedPropertyDefinition.set = function () {
      warn(
        ("Computed property \"" + key + "\" was assigned to but it has no setter."),
        this
      );
    };
  }
  // 将sharedPropertyDefinition绑定到组件对象上
  Object.defineProperty(target, key, sharedPropertyDefinition);
}
复制代码

😅感受有点乱,最后再梳理下上边的逻辑:

initComputed

  • 执行initComputed,从Vue中拿到computed对象里全部的key
  • 循环拿到的key值,调用defineComputed函数,把computed绑定到组件对象上

defineComputed

  • 判断是否在服务端渲染,是则computed的结果会被缓存,不是则不会缓存计算结果
  • 因为computed存在两种写法,这里也对函数对象的写法作了区分

computed的结果缓存是如何实现的?

上面咱们大体梳理了下computed的初始化逻辑,如今咱们回过头来再看一下官方定义,发现其中提到了计算属性会将计算结果缓存下来,那么这个计算结果究竟是怎么被缓存下来的呢?

回到defineComputed

defineComputed里最后将sharedPropertyDefinition绑定到组件对象上,在代码里面能够看到对sharedPropertyDefinition.get作了特殊处理,两种状况分别封装了:

  • createComputedGetter
  • createGetterInvoker

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
    }
  }
}
复制代码

createGetterInvoker的实现:

function createGetterInvoker(fn) {
  return function computedGetter () {
    return fn.call(this, this)
  }
}
复制代码

能够看到,服务端渲染确实是对计算属性的结果不作缓存的,可是咱们对结果是如何缓存,依旧是一脸懵逼😐

回到最初的断点

刷新页面回到一开始咱们在appProxy中打下的断点,在调用栈中有两个显眼的函数:

  • evaluate
  • get

分别点进去,咱们能够看到:

evaluate实现源码:

Watcher.prototype.evaluate = function evaluate () {
  this.value = this.get();
  this.dirty = false;
};
复制代码

get实现源码:

Watcher.prototype.get = function get () {
  pushTarget(this);
  var value;
  var vm = this.vm;
  try {
    value = this.getter.call(vm, vm);
  } catch (e) {
    if (this.user) {
      handleError(e, vm, ("getter for watcher \"" + (this.expression) + "\""));
    } else {
      throw e
    }
  } finally {
    // "touch" every property so they are all tracked as
    // dependencies for deep watching
    if (this.deep) {
      traverse(value);
    }
    popTarget();
    this.cleanupDeps();
  }
  return value
};
复制代码

结合上面给出的createComputedGetter源码咱们能够知道,computed的计算结果是经过Watcher.prototype.get来获得的,拿到value之后,在Wathcer.prototype.evaluate中执行了这样一行代码:

...
this.dirty = false;
复制代码

聪明的读者确定猜到了,计算属性是否从新计算结果,确定跟这个属性有关。接下来咱们只要跟踪这个属性的变化,就能够轻松的知道计算属性的缓存原理了。

扫描下方的二维码或搜索「tony老师的前端补习班」关注个人微信公众号,那么就能够第一时间收到个人最新文章。

相关文章
相关标签/搜索