• 7.10 computed
    • 7.10.1 依赖收集
    • 7.10.2 派发更新

    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,又会对计算属性重新求值。