Vue 初始化

2022/11/6 Vue

不管我们在使用 Vue Cli 生成的框架,还是直接 script 标签引入的 Vue,在使用 Vue 的时候,都需要 new Vue(); 原因上一篇已经讲过,Vue 是一个函数类,只能通过 new 来创建一个实例,从而来使用。

这篇我们来看一下 Vue 初始化的过程,我们在第一篇里创建了调试环境,我们就在 new Vue 的时候打一个断点,来看看 Vue 初始化都做了哪些事情。

初始化

在 new Vue() 的时候,进入到了 Vue 的方法里,并执行了this._init(options)这个方法, 这个方法是从哪来的呢?

我们继续调试,进入了initMixin 这个方法里,我们来研究一下这个方法, 这个方法 在/core/instance/init.ts

import config from '../config';
import { initProxy } from './proxy';
import { initState } from './state';
import { initRender } from './render';
import { initEvents } from './events';
import { mark, measure } from '../util/perf';
import { initLifecycle, callHook } from './lifecycle';
import { initProvide, initInjections } from './inject';
import { extend, mergeOptions, formatComponentName } from '../util/index';
import type { Component } from 'types/component';
import type { InternalComponentOptions } from 'types/options';
import { EffectScope } from 'v3/reactivity/effectScope';

let uid = 0;

// 初始化mixin
export function initMixin(Vue: typeof Component) {
  // _init 方法 只有一个参数 options
  Vue.prototype._init = function (options?: Record<string, any>) {
    const vm: Component = this;
    // a uid
    vm._uid = uid++;

    // 用于测试代码性能
    let startTag, endTag;
    /* istanbul ignore if */
    if (__DEV__ && config.performance && mark) {
      startTag = `vue-perf-start:${vm._uid}`;
      endTag = `vue-perf-end:${vm._uid}`;
      mark(startTag);
    }

    // 一个标志来标记它为Vue实例,从而不用执行instanceof检查
    vm._isVue = true;
    // 避免实例被观察到
    vm.__v_skip = true;
    // effect scope
    vm._scope = new EffectScope(true /* detached */);
    vm._scope._vm = true;
    // 合并 options
    // 有子组件时,_isComponent 为true
    if (options && options._isComponent) {
      // 优化内部组件实例化,因为动态选项合并非常慢,而且没有任何内部组件选项需要特殊处理。
      initInternalComponent(vm, options as any);
    } else {
      // Vue 实例不是组件时
      // 就会给$options 属性赋值一个 合并的options
      vm.$options = mergeOptions(
        // 解析constructor上的options属性的
        resolveConstructorOptions(vm.constructor as any),
        options || {}, // 传入的 options
        vm, // 该实例 this
      );
    }
    /* istanbul ignore else */
    // 给 _renderProxy 属性赋值,不同环境赋值的结果不同
    if (__DEV__) {
      initProxy(vm);
    } else {
      // 属性指向 vue 实例本身
      vm._renderProxy = vm;
    }
    // expose real self
    vm._self = vm;
    initLifecycle(vm); // 初始化生命周期相关字段
    initEvents(vm); // 初始化事件列表
    // 处理插槽和作用于插槽,提供_c和$createElement函数用于创建VNode,接收父组件传递$attrs和$listeners作为响应式数据
    initRender(vm);
    callHook(vm, 'beforeCreate', undefined, false /* setContext */); //执行 beforeCreate生命周期钩子
    initInjections(vm); // 在 data/props 之前初始化 inject
    initState(vm); // // 初始化 data,props,methods computed,watch
    initProvide(vm); // resolve provide after data/props
    callHook(vm, 'created'); // 执行 created 生命周期钩子

    /* istanbul ignore if */
    if (__DEV__ && config.performance && mark) {
      vm._name = formatComponentName(vm, false);
      mark(endTag);
      measure(`vue ${vm._name} init`, startTag, endTag);
    }

    if (vm.$options.el) {
      vm.$mount(vm.$options.el);
    }
  };
}

// 初始化合并选项
// 主要做两件事: 1、指定组件 $options 原型
//             2、把组件依赖于父组件的props、listeners也挂载到options上,方便子组件调用
export function initInternalComponent(
  vm: Component, // 组件实例 即this
  options: InternalComponentOptions, // createComponentInstanceForVnode中定义的组件options
) {
  // 把组件构造函数的 options 挂载到 vm.$options 的 __proto__ 上
  const opts = (vm.$options = Object.create((vm.constructor as any).options));
  // 把传入参数的 option 的 _parentVode 和 parent 挂载到组件实例 $options 上
  // 这样做是因为它比动态枚举更快.
  const parentVnode = options._parentVnode; // 组件 Vnode 对象
  opts.parent = options.parent; // 根实例
  opts._parentVnode = parentVnode;

  // 父组件里的 vnode 上的
  // propsData,listeners,children,tag属性挂载到 $options 上
  const vnodeComponentOptions = parentVnode.componentOptions!;
  opts.propsData = vnodeComponentOptions.propsData; // props
  opts._parentListeners = vnodeComponentOptions.listeners; // 子组件emit出来的方法
  opts._renderChildren = vnodeComponentOptions.children;
  opts._componentTag = vnodeComponentOptions.tag;

  // 如果传入的 option 中如果有 render,把render相关的也挂载到 $options 上
  if (options.render) {
    opts.render = options.render;
    opts.staticRenderFns = options.staticRenderFns;
  }
}

// 返回类的构造函数上最新的options值
// 参数是实例的构造函数 vm.constructor
export function resolveConstructorOptions(Ctor: typeof Component) {
  let options = Ctor.options;
  // 根实例没有 super,extend 生成的子组件是有
  // 如果有 super
  if (Ctor.super) {
    // 寻找父类的 options
    // 通过递归,把上一级父类上最新的 options 更新到变量 superOptions 中。
    const superOptions = resolveConstructorOptions(Ctor.super);
    // 把 extend 时父类的 options,即 Ctor.superOptions 赋值给 cachedSuperOptions 变量。
    const cachedSuperOptions = Ctor.superOptions;
    // 如果父类的 options 发生了改变
    // 可能是被全局 mixins 混入了其他内容
    if (superOptions !== cachedSuperOptions) {
      // 更新 options
      Ctor.superOptions = superOptions;
      // 判断现有子类 option 和在 extend 中挂载到子类 option 中的 sealedOptions是否一样
      const modifiedOptions = resolveModifiedOptions(Ctor);
      // 更新 extendOptions
      if (modifiedOptions) {
        extend(Ctor.extendOptions, modifiedOptions);
      }
      // 然后获取最新的 options
      options = Ctor.options = mergeOptions(superOptions, Ctor.extendOptions);
      if (options.name) {
        options.components[options.name] = Ctor;
      }
    }
  }
  // 返回 global-api 里设置的 vue.options
  return options;
}

// 判断子类的 options 和从 extend 中挂载的 sealedOptions 是否一致
function resolveModifiedOptions(Ctor: typeof Component): Record<string, any> | null {
  let modified;
  const latest = Ctor.options;
  const sealed = Ctor.sealedOptions;
  for (const key in latest) {
    if (latest[key] !== sealed[key]) {
      if (!modified) modified = {};
      modified[key] = latest[key];
    }
  }
  return modified;
}

在 initMixin 函数中,就给 Vue 类的原型上绑定_init 方法。

// 合并 options
// 有子组件时,_isComponent 为true
if (options && options._isComponent) {
  // 优化内部组件实例化,因为动态选项合并非常慢,而且没有任何内部组件选项需要特殊处理。
  initInternalComponent(vm, options as any)
} else {
  // Vue 实例不是组件时
  // 就会给$options 属性赋值一个 合并的options
  vm.$options = mergeOptions(
    // 解析constructor上的options属性的
    resolveConstructorOptions(vm.constructor as any),
    options || {}, // 传入的 options
    vm // 该实例 this
  )
}
...

把用户传递的 options 选项与当前构造函数的 options 属性及其父级构造函数的 options 属性进行合并,得到一个新的 options 选项赋值给$options 属性,并将$options 属性挂载到 Vue 实例上

// 给 _renderProxy 属性赋值,不同环境赋值的结果不同
if (__DEV__) {
  initProxy(vm);
} else {
  // 属性指向 vue 实例本身
  vm._renderProxy = vm;
}

我们看了一下 initProxy,其实也是在给 _renderProxy 赋值。全局搜了一下:

// src\core\instance\render.ts
vnode = render.call(vm._renderProxy, vm.$createElement);

_renderProxy 是渲染函数 render 的执行上下文,在生产环境下,执行上下文就是实例本身,而在开发环境下,执行上下文则使用 initProxy 进行了处理,这个我们后面再看。

// expose real self
vm._self = vm;
initLifecycle(vm); // 初始化生命周期相关字段
initEvents(vm); // 初始化事件列表
// 处理插槽和作用于插槽,提供_c和$createElement函数用于创建VNode,
// 接收父组件传递$attrs和$listeners作为响应式数据
initRender(vm);
callHook(vm, 'beforeCreate', undefined, false /* setContext */); //执行 beforeCreate生命周期钩子
initInjections(vm); // 在 data/props 之前初始化 inject
initState(vm); // // 初始化 data,props,methods computed,watch
initProvide(vm); // resolve provide after data/props
callHook(vm, 'created'); // 执行 created 生命周期钩子

下面的代码,则是调用一些初始化函数来为 Vue 实例初始化一些属性,事件,响应式数据,并且调用了beforeCreate 和 created 的钩子函数

if (vm.$options.el) {
  vm.$mount(vm.$options.el);
}

初始化完成以后,会根据是否传入了 el 选项,如果传入了则调用$mount函数进入模板编译与挂载阶段,如果没有传入el选项,则不进入下一个生命周期阶段,需要用户手动执行vm.$mount方法才进入下一个生命周期阶段。

综上 Vue 初始化主要是合并配置,初始化生命周期,初始化事件,初始化渲染,初始化数据等。