Vue源码分析-computed、watch

207 阅读1分钟

computed

computed 的初始化在core/instance/state.js中:

export function initState(vm: Component) {
  vm._watchers = [];
  const opts = vm.$options;
  if (opts.props) initProps(vm, opts.props);
  if (opts.methods) initMethods(vm, opts.methods);
  if (opts.data) {
    initData(vm);
  } else {
    observe((vm._data = {}), true /* asRootData */);
  }
  if (opts.computed) initComputed(vm, opts.computed);
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch);
  }
}

可以看到 computed 的初始化和 props、data、methods、watch 是一起的。 initComputed 方法遍历 computed options,如果未存在该 computed key,新建一个 Watcher 对象(lazy = true),将用户定义的函数传入到 Watcher 中(后续 Watcher 通过 this.get 访问),以下称该 Watcher 为 Computed Watcher;

function initComputed(vm: Component, computed: Object) {
  const watchers = (vm._computedWatchers = Object.create(null));
  const isSSR = isServerRendering();
  for (const key in computed) {
    const userDef = computed[key];
    const getter = typeof userDef === "function" ? userDef : userDef.get;
    if (!isSSR) {
      watchers[key] = new Watcher(
        vm,
        getter || noop,
        noop,
        {lazy: true}
      );
    }
    if (!(key in vm)) {
      defineComputed(vm, key, userDef);
    }
  }
}

因为设置了 lazy = true,所以该 Computed Watcher 不会立即计算值,可以看下 Watcher 构造函数 constructor 中关于这块的处理,可以看到如果是 Computed Watcher 的话,初始化不会有任何操作:

this.value = this.lazy ? undefined : this.get();

definedComputed 主要是修改了 computed 的 get 函数,当调用 computed 时就不会直接执行用户定义的方法,而是相当于走了一个代理,这样就可以实现 computed 的缓存,以下为核心代码实现:

export function defineComputed(
  target: any,
  key: string,
  userDef: Object | Function
) {
  sharedPropertyDefinition.get = createComputedGetter(key)
  sharedPropertyDefinition.set = noop;
  Object.defineProperty(target, key, sharedPropertyDefinition);
}

可以看到使用 createComputedGetter 来代替了本身的 get 方法,当调用计算属性时,就会执行这里返回的方法,这块是 Computed Watcher 的核心,下面我们重点分析一下该方法,先看下方法核心代码:

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;
    }
  };
}
  1. 如果 watcher.dirty = true,也就是初始化或者需要更新的时候,此时会执行 watcher.evaluate 方法,看下该方法的核心代码:
evaluate() {
  this.value = this.get();
  this.dirty = false;
}

get() {
    pushTarget(this);
    let value;
    const vm = this.vm;
    value = this.getter.call(vm, vm);
    popTarget();
    return value;
  }

主要是执行 getter 方法,也就是用户定义的函数,如果用户定义的函数中使用了响应式 data,那么该 data deps 会收集当前 Computed Watcher(注意这里的 pushTarget 就是将 Dep.target 设置为该 Computed Watcher 从而使 data deps 收集到该 Watcher),最终返回值;

计算结束后会将 dirty 设置为 false,也就是说下次访问该 computed 时不会重新计算,那么 dirty 什么时候再变为 ture 呢?答案是 compute 中的响应式 data 发生改变时,会触发 deps.notify,方法中对所有的 Watcher 进行 update,上面说了该 Computed Watcher 已被函数内的 data deps 所收集,所以当任何一个 data 发生改变时,会触发 watcher.update,看下该方法定义:

update() {
  /* istanbul ignore else */
  if (this.lazy) {
    this.dirty = true;
  } else if (this.sync) {
    this.run();
  } else {
    queueWatcher(this);
  }
}

Computed Watcher 的 lazy = true,所以 update 方法只会将 lazy 设为 true,也就是下次更新要重新计算;

  1. 那这个下次是哪次呢,难道要等用户重新获取 computed 值吗?那显然不可能,刚上面说了用户定义的函数内的 data deps 除了搜集了该 Computed Watcher,也收集了 Render Watcher,所以当 data 发生改变时,会触发组件重新执行 render 函数,也就是一定会再调用一下 computed 的值。data 收集 Render 的过程就在 watcher.depend 方法中:
depend() {
  let i = this.deps.length;
  while (i--) {
    this.deps[i].depend();
  }
}

可以看到因为 deps 和 watcher 是双向依赖的,所以 Computed Watcher 中包含了所有用户定义的函数内的 data deps,此时再对这些 data deps 做一次依赖收集,因为此时的 Dep.target 就是 Render Watcher 了,所以 Render Watcher 也都被收集到 data deps 中。

总结一下:

  1. computed 会新建一个 lazy = true 的 Watcher,该 Watcher 不会立即执行,所以 computed 内用户定义的方法不会立即执行;
  2. 使用 Object.defineProperty 将 key 绑定在 vm 上,get 方法主要判断是否需要重新计算,并使函数内的 data deps 收集 Computed Watcher 和 Render Watcher;
  3. computed 内 data 值发生改变时,dirty = false,并且会触发 render function,会重新触发 computed 的 get 方法,此时不会取缓存值而是更新 computed 值。

watch

只要理解了上面 computed 的流程,再看 watch 这里的实现会简单很多,watch 的初始化在 initState 的最后(这样就可以监听 computed 咯),initWatch 方法主要调用了 createWatcher,createWatcher 中又主要调用了 vm.$watch方法...实际上watch选项中就是调用了vm.$watch,看下 vm.$watch 的实现:

Vue.prototype.$watch = function (
    expOrFn: string | Function,
    cb: any,
    options?: Object
  ): Function {
    const vm: Component = this;
    if (isPlainObject(cb)) {
      return createWatcher(vm, expOrFn, cb, options);
    }
    options = options || {};
    options.user = true;
    const watcher = new Watcher(vm, expOrFn, cb, options);
    if (options.immediate) {
      try {
        cb.call(vm, watcher.value);
      } catch (error) {
      }
    }
    return function unwatchFn() {
      watcher.teardown();
    };
  };

其实就是 new 了一个 Watcher 对象,将 watch 的 key 作为 expOrFn,value 作为 cb。 注意这里的 Watcher 和上面的 Computed Watcher 不同,它会立即执行 getter 方法,也就是会立即执行 expOrFn,正常情况下这玩意就是个 data,还记得上面的 watcher.get 方法不,会将当前的 Watcher 放到 data deps 中。所以当初始化完 watch 之后,watch 中监听的 data deps 就会收集了该 Watcher,所以 data 更新后会触发该 Watcher 更新:

update() {
  /* istanbul ignore else */
  if (this.lazy) {
    this.dirty = true;
  } else if (this.sync) {
    this.run();
  } else {
    queueWatcher(this);
  }
}

会执行 queueWatcher,将该 Watcher 放入到一个队列中,等到恰当时机再依次执行,那是会执行 cb,这里不详细介绍了。

这里解释下为啥 watch 能监听 computed 的值,答案就是 computed 的 get 方法中watcher.depend();

正常的 computed 中,data deps 收集完了 Computed Watcher 后 Dep.target 会指向 Render Watcher,所以执行该方法会再次收集 Render Watcher;

而在 watch 监听 computed 时,data deps 收集完了 Computed Watcher 后 Dep.target 会指向当前 Watcher,所以执行该方法会再次收集当前 Watcher。所以当 computed 中 data 更新时,会先触发 Computed Watcher 将 dirty = false,然后触发当前 Watcher,当前 Watcher 会重新执行 this.get,也就是会重新计算 computed 中的值,然后将新老值传给 cb。