Vue2源码分析(五):computed & watch

1,470 阅读2分钟

本篇会分析Vue2 中 computed 的创建过程

先看我们的demo:

<template>
  <section>
    <div>param1: {{ param1 }}</div>
    <div>param2: {{ param2 }}</div>
    <div>sum: {{ sum }}</div>
    <div>操作:
      <button @click="handlehangeRawBtn(1)" name="button">make param1 ++</button>
      <button @click="handlehangeRawBtn(2)" name="button">make param2 ++</button>
      <button @click="handleConcleComputedData" name="button">将param1 的值复制成打印模板中未用到的computed  minus 的值</button>
    </div>
    <section>
    </section>
  </section>
</template>

<script>
import child1 from './components/child1.vue'
export default {
  name: 'demo',
  components: { child1 },
  data() {
    return {
      param1: 1,
      param2: 2,
      param3: 3,
    }
  },
  computed: {
    sum({ param1, param2 }) {
      return param1 + param2
    },
    minus({ param3, param2 }) {
      return param3 - param2
    }
  },
  watch: {
    param3(val) {
      this.param1 = val
    }
  },
  mounted() {
    console.log('App mounted this', this)
  },
  methods: {
    handlehangeRawBtn(type) {
      this[`param${type}`]++
    },
    handleConcleComputedData() {
      this.param1 = this.minus
    },
  }
}
</script>

initComputed 源码:

const sharedPropertyDefinition = {
  enumerable: true,
  configurable: true,
  get: noop,
  set: noop
}
function initComputed (vm: Component, computed: Object) {
  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.
      // 为computed 的没个key去创建watcher 实例,watcher的第二个参数就是computed 定义的函数/get函数
      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.
    /*
        根据上篇文章,组件在继承的时候(VueComponent的调用)就建立了props 和 computed 数据的代理,
        定义了对应的 getter, setter 函数, 在实例化的这里就是真正去创建了computed watcher
    */
    if (!(key in 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)
      }
    }
  }
}

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)
}

function createComputedGetter (key) {
  return function computedGetter () {
    const watcher = this._computedWatchers && this._computedWatchers[key]
    if (watcher) {
      if (watcher.dirty) {
        watcher.evaluate()
      }
      if (Dep.target) {
        watcher.depend()
      }
      return watcher.value
    }
  }
}

其实看源码也能知道computed 并不是所有都会走缓存。

下图就是组件继承时(function VueComponent 的调用),对props 和 computed 属性建立数据代理的过程 image.png

在实例化的时候,就如代码所示,去创建真正的computed watcher, 
我们接下来以 sumComputedWatcher 代表为 sum 创建的watcher 实例,
render函数的调用,用到了vm.sum, 首先会触发了绑定在原型上的vm.sum.getter的劫持代理,
走到了对应computedGetter 的调用
( 如果你没理解上篇中,为什么要在VueComponent 调用 initComputed 和 initProps,
  以及为什么要建立Sub Vue 来继承Vue, 这里其实就能慢慢明白。
  越看越觉得尤大yyds
 )

不要把原型上的computed getter, sumComputedWatcher.get() 以及 sumComputedWatcher.getter()搞混淆。

image.png

image.png

1| sumComputedWatcher.evaluate()
    /* class Watcher {
        ...,
        get() {
          pushTarget(this);
          let value;
          const 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;
        },
        evaluate() {
          this.value = this.get();
          /* 调用 sumComputedWatcher 的get函数,将Dep.target推入targetStack,
              接着调用sumComputedWatcher 的getter 函数,
              这里不要把watcher类 的get 函数和watcher的劫持 getter函数搞混淆
          */
          this.dirty = false; // dirty属性变为false
        }
        ...,
    }
    */
2| sumComputedWatcher.get()
3| pushTarget()
4| sumComputedWatcher.getter.call(vm, vm)//等同于 sum(vm),这里也就能看出来为什么computed函数内直接能拿到vm上的变量
    /*
        sum({ param1, param2 }) {
            return param1 + param2
          }
      其实看到computed函数就能明白,sum的调用,依赖了param1 和 param2,
      要做加法运算,首先得去 vm上 拿到这两个变量,那就势必会触发 param1 和param2 的数据劫持.
      (param1 和 param2 早就在initData时创建了自己的Observer 实例, 从调用栈也能明显的看出来这个关系)
    */

看下图,sum 的调用分别触发了param1Dep 和 param2Dep的get。

image.png

接着就开始了收集依赖的一系列过程

image.png

image.png

这个过程完成了sumComputedWatcher 和 param1Dep, param2Dep 之间发布订阅关系的建设,接着函数的调用

image.png

调用完watcher.evaluate();后接着又调用了watcher.depend(),经过前面的章节,以我们的例子来说 watcher.depend()无非就是又去收集render watcher 和param1,param2 的关系,明明在render 函数调用时收集过了,为什么还要去收集?这里其实也很好理解,模板中用到的变量并不一定是vm初始化时定义的所有变量,就拿computed 来说,我可以定义供method调用。对于method用computed 的变量,就和render 函数调用时一个道理,在首次调用时收集依赖。

这里其实可以理解为computed watcher 是在调用时完成依赖收集的过程的。

接着我们看下watch 的初始化

function initWatch(vm, watch) {
    for (const key in watch) {
      const handler = watch[key];

      if (Array.isArray(handler)) {
        for (let i = 0; i < handler.length; i++) {
          createWatcher(vm, key, handler[i]);
        }
      } else {
        createWatcher(vm, key, handler);
      }
    }
  }
  function createWatcher(vm, keyOrFn, handler, options) {
    if (isPlainObject(handler)) {
      options = handler;
      handler = handler.handler;
    }

    if (typeof handler === 'string') {
      handler = vm[handler];
    }

    return vm.$watch(keyOrFn, handler, options); // 调用$watch 函数
  }
  Vue.prototype.$watch = function (expOrFn, cb, options) {
      const vm = this;

      if (isPlainObject(cb)) {
        return createWatcher(vm, expOrFn, cb, options);
      }

      options = options || {};
      options.user = true;
      const watcher = new Watcher(vm, expOrFn, cb, options); // 创建watcher 实例

      if (options.immediate) {
        cb.call(vm, watcher.value);
      }

      return function unwatchFn() {
        watcher.teardown();
      };
    };
  }

在组件初始化的时候就已经完成了param3Wacter的依赖收集,从watch 的key中分析到自己的依赖。 对比computed 来说,computed watcher 在大多数情况下会有缓存,只有真正依赖的数据变化时,才会调用更新,但watch 更多的思想是观察自己的数据去做一些操作。

写到这里已经不想写了....