vue中created、watch和computed的执行顺序

发布时间 2023-11-17 15:52:03作者: 柯基与佩奇

总结

关于 vue 中 created 和 watch 的执行顺序相对比较简单,而其中 computed 是通过 Object.defineProperty 为当前 vm 进行定义,再到后续创建 vNode 阶段才去触发执行其 get 函数,最终执行到计算属性 computed 对应的逻辑。

官网的生命周期图中,init reactivity 是晚于 beforeCreate 但是早于 created 的。
watch 加了 immediate,应当同 init reactivity 周期一同执行,早于 created。
而正常的 watch,则是 mounted 周期后触发 data changes 的周期执行,晚于 created。

先看个简单的例子:

// main.js
import Vue from "vue";

new Vue({
  el: "#app",
  template: `<div>
    <div>{{computedCount}}</div>
  </div>`,
  data() {
    return {
      count: 1,
    };
  },
  watch: {
    count: {
      handler() {
        console.log("watch");
      },
      immediate: true,
    },
  },
  computed: {
    computedCount() {
      console.log("computed");
      return this.count + 1;
    },
  },
  created() {
    console.log("created");
  },
});

当前例子的执行顺序为:watch --> created --> computed。

为什么?
在 new Vue 的实例化过程中,会执行初始化方法 this._init,其中有代码:

Vue.prototype._init = function (options) {
  // ...
  initState(vm);
  // ...
  callHook(vm, "created");
  // ...
  if (vm.$options.el) {
    vm.$mount(vm.$options.el);
  }
};

function initState(vm) {
  vm._watchers = [];
  var 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);
  }
}

猛一看代码,是不是发现先执行的 initComputed(vm, opts.computed),然后执行 initWatch(vm, opts.watch),再执行 callHook(vm, 'created'),那为啥不是 computed --> watch --> created 呢?

1、关于 initComputed

const computedWatcherOptions = { lazy: true };
function initComputed(vm: Component, computed: Object) {
  // $flow-disable-line
  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 (!isSSR) {
      // create internal watcher for the computed property.
      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.
    if (!(key in vm)) {
      defineComputed(vm, key, userDef);
    } else if (process.env.NODE_ENV !== "production") {
      // ...
    }
  }
}

在通过 initComputed 初始化计算属性的时候,通过遍历的方式去处理当前组件中的 computed。首先,在进行计算属性实例化的时候,将{ lazy: true }作为参数传入,并且实例化的 Watcher 中的 getter 就是当前例子中的 computedCount 函数;其次,通过 defineComputed(vm, key, userDef)的方式在当前组件实例 vm 上为 key 进行 userDef 的处理。具体为:

export function defineComputed(
  target: any,
  key: string,
  userDef: Object | Function
) {
  const shouldCache = !isServerRendering();
  if (typeof userDef === "function") {
    sharedPropertyDefinition.get = shouldCache
      ? createComputedGetter(key)
      : createGetterInvoker(userDef);
    sharedPropertyDefinition.set = noop;
  }
  // ...
  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;
    }
  };
}

从以上可以看出,这里通过 Object.defineProperty(target, key, sharedPropertyDefinition)的方式,将函数 computedGetter 作为 get 函数,只有当对 key 进行访问的时候,才会触发其内部的逻辑。内部逻辑 watcher.evaluate()为:

evaluate () {
    this.value = this.get()
    this.dirty = false
}

get 中有主要逻辑:

value = this.getter.call(vm, vm);

这里的 this.getter 就是当前例子中的:

computedCount() {
  console.log('computed');
  return this.count + 1;
}

也就是说,只有当获取 computedCount 的时候才会触发 computed 的计算,也就是在进行 vm.$mount(vm.$options.el)阶段才会执行到 console.log('computed')。

2、关于 initWatch

function initWatch(vm: Component, watch: Object) {
  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: Component,
  expOrFn: string | Function,
  handler: any,
  options?: Object
) {
  if (isPlainObject(handler)) {
    options = handler;
    handler = handler.handler;
  }
  if (typeof handler === "string") {
    handler = vm[handler];
  }
  return vm.$watch(expOrFn, handler, options);
}

在通过 initWatch 初始化侦听器的时候,如果 watch 为数组,则遍历执行 createWatcher,否则直接执行 createWatcher。如果 handler 是对象或者字符串时,将其进行处理,最终作为参数传入 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) {
      handleError(
        error,
        vm,
        `callback for immediate watcher "${watcher.expression}"`
      );
    }
  }
  return function unwatchFn() {
    watcher.teardown();
  };
};

这里获取到的 options 中会有 immediate: true 的键值,同时通过 options.user = true 设置 user 为 true,再将其作为参数传入去进行 Watcher 的实例化。
当前例子中 options.immediate 为 true,所以会执行 cb.call(vm, watcher.value),也就是以 vm 为主体,立刻执行 cb。当前例子中 cb 就是 handler:

handler() {
    console.log('watch');
},

这里就解释了当前例子中 console.log('watch')是最先执行的。
然后,执行完 initComputed 和 initWatch 以后,就会通过 callHook(vm, 'created')执行到生命周期中的 console.log('created')了。
最后通过 vm.$mount(vm.$options.el)进行页面渲染的时候,会先去创建 vNode,这时就需要获取到 computedCount 的值,进而触发其 get 函数的后续逻辑,最终执行到 console.log('computed')。