面试官:new Vue()都发生了什么

大家好,今天的分享由团队的 uncle13 老师提供。

先来一张比较流行的图:

在 Vue.js 中,当使用 new Vue 创建一个新的 Vue 实例时,会触发一系列的初始化过程,用于设置和管理组件的状态、响应式数据、渲染等。以下是使用 new Vue 创建实例时发生的主要步骤:

  1. 实例的创建

    • 调用 Vue 构造函数创建一个 Vue 实例,同时进行初始化配置的合并和属性初始化。
  2. 初始化生命周期钩子

    • 初始化 Vue 实例的生命周期钩子,如 beforeCreatecreated 等,用于在组件的不同生命周期阶段执行自定义逻辑。
  3. 初始化事件系统

    • 初始化 Vue 实例的事件系统,包括 $on$emit$off 等方法,用于实现组件之间的通信。
  4. 初始化注入和状态

    • 处理注入(provide/inject)和响应式数据(data)的初始化,将注入的数据和响应式数据挂载到实例上。
  5. 初始化渲染函数

    • 解析模板或 render 函数,生成虚拟 DOM(VNode),用于渲染组件的视图。
  6. 初始化渲染相关的属性

    • 初始化 $attrs$listeners,用于在组件中处理继承的属性和监听器。
  7. 初始化计算属性和方法

    • 初始化计算属性(computed)和方法(methods),将它们挂载到 Vue 实例上。
  8. 初始化侦听属性

    • 初始化侦听属性(watch),用于监听数据的变化并执行相应的回调。
  9. 初始化组件的 props

    • 解析组件的 props 配置,将 props 对象挂载到 Vue 实例上。
  10. 调用 beforeCreate 钩子

    • 在实例创建完成后,但数据和事件都未初始化时调用 beforeCreate 钩子。
  11. 初始化注入

    • beforeCreate 钩子中,会处理注入的数据,将注入的数据挂载到实例上。
  12. 初始化响应式数据

    • beforeCreate 钩子中,会将响应式数据进行初始化,使其具有响应式特性。
  13. 调用 created 钩子

    • 在实例创建完成且数据和事件都已初始化时调用 created 钩子。
  14. 编译模板(如果有):

    • 如果 Vue 实例中定义了模板选项,会将模板编译成渲染函数,用于渲染视图。
  15. 挂载实例

    • 调用 $mount 方法来挂载 Vue 实例到页面上的 DOM 元素上,触发组件的渲染。
  16. 调用 beforeMount 钩子

    • 在实例挂载到 DOM 元素之前调用 beforeMount 钩子。
  17. 执行渲染函数

    • 调用之前编译生成的渲染函数,生成虚拟 DOM 并渲染到页面上。
  18. 调用 mounted 钩子

    • 在实例挂载到 DOM 元素之后调用 mounted 钩子。

源码学习

创建一个 Vue 实例时,涉及到许多初始化过程,包括响应式数据、渲染函数的编译、生命周期钩子的调用等。以下是相关源码的简要解析,针对 Vue 2.x 版本:

  1. 创建 Vue 实例

    function Vue(options{
      if (!(this instanceof Vue)) {
        warn('Vue is a constructor and should be called with the `new` keyword');
      }
      this._init(options);
    }

    在这个步骤中,Vue 构造函数被调用,同时通过 _init 方法进行初始化。

  2. 初始化 Vue 实例

    export function initMixin(Vue{
      Vue.prototype._init = function (options{
        const vm = this;

        // 合并配置
        vm.$options = mergeOptions(
          resolveConstructorOptions(vm.constructor),
          options || {},
          vm
        );

        // 初始化生命周期钩子
        initLifecycle(vm);

        // 初始化事件
        initEvents(vm);

        // 初始化渲染函数
        initRender(vm);

        // 调用 beforeCreate 钩子
        callHook(vm, 'beforeCreate');

        // 初始化注入
        initInjections(vm);

        // 初始化响应式数据
        initState(vm);

        // 初始化侦听属性
        initProvide(vm);

        // 调用 created 钩子
        callHook(vm, 'created');

        // 挂载实例
        if (vm.$options.el) {
          vm.$mount(vm.$options.el);
        }
      };
    }

    在初始化阶段,以下几个重要步骤值得关注:

    • mergeOptions:合并 Vue 实例构造函数的选项和用户传入的选项。
    • initLifecycle:初始化生命周期钩子,如 $parent$children 等。
    • initEvents:初始化事件系统,如 $on$emit 等。
    • initRender:初始化渲染函数,设置 $slots$createElement 等。
    • initInjectionsinitState:初始化注入和响应式数据。
    • initProvide:处理 provideinject
    • $mount:如果指定了 el,则挂载实例到 DOM 元素上。
  3. 初始化响应式数据

    export function initState(vm{
      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);
      }
      if (opts.computed) initComputed(vm, opts.computed);
      if (opts.watch && opts.watch !== nativeWatch) {
        initWatch(vm, opts.watch);
      }
    }

    这一步主要负责初始化组件的响应式数据。它会根据 data 选项初始化数据,并通过 Object.defineProperty 实现数据的响应式。同时,也会处理 propsmethodscomputedwatch 等选项。

  4. 初始化渲染函数

    export function initRender(vm{
      vm._vnode = null;
      vm._staticTrees = null;
      const options = vm.$options;
      const parentVnode = (vm.$vnode = options._parentVnode);
      const renderContext = parentVnode && parentVnode.context;
      vm.$slots = resolveSlots(options._renderChildren, renderContext);
      vm.$scopedSlots = emptyObject;

      // 创建 createElement 和 $createElement 方法
      vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false);
      vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true);

      // 定义 $attrs 和 $listeners
      const parentData = parentVnode && parentVnode.data;
      defineReactive(vm, '$attrs', parentData && parentData.attrs || emptyObject, nulltrue);
      defineReactive(vm, '$listeners', options._parentListeners || emptyObject, nulltrue);
    }

    在这一步中,会初始化渲染函数相关的属性,包括 $slots$scopedSlots$createElement 等。同时也会定义 $attrs$listeners,用于处理继承的属性和监听器。

简单来说,Vue实例的过程中主要做了两件事:一个是初始化vm(各种事件,参数等),一个是挂载Vue实例到'#app'上。

相关问题:

1. 为什么 this 直接访问到 methods 里面的函数?

在 Vue.js 中,可以在组件的模板中直接访问 methods 中定义的函数,是因为 Vue 在编译模板的过程中会将 methods 中的函数绑定到组件实例上,使其可以通过 this 直接访问。

这种行为是 Vue 的特性之一,旨在让开发者可以在模板中方便地调用组件实例的方法。当模板编译时,Vue 会将 methods 中的方法添加到组件实例上,使其成为组件实例的属性,因此可以通过 this 访问。

下面是一个简单的示例,说明为什么可以通过 this 直接访问到 methods 中的函数:

new Vue({
  el'#app',
  data: {
    message'Hello, Vue!'
  },
  methods: {
    greet() {
      console.log(this.message);
    }
  }
});

在这个示例中,methods 中的 greet 方法被绑定到了组件实例上。在模板中,你可以这样调用这个方法:

<div id="app">
  <button @click="greet">Say Hello</button>
</div>

在点击按钮时,Vue 会通过组件实例调用 greet 方法,并在方法内部通过 this.message 访问到 data 中的 message 属性。

需要注意的是,尽管可以通过 this 直接访问 methods 中的函数,但在模板中访问 data 中的属性时,需要使用插值语法或指令,如 {{ message }}v-bind。这是因为 Vue 在模板编译时会对数据绑定进行特殊处理。

2. 为什么 this 直接访问到 data 里面的数据

在 Vue.js 中,this 直接访问到 data 里面的数据是因为 Vue 在实例化组件时,会将 data 中的属性代理到组件实例上,从而可以通过 this 直接访问。

这种代理行为使得在组件的各个方法和模板中都可以方便地访问和操作 data 中的数据,而不需要显式通过实例的属性或方法来访问。

以下是一个示例,说明为什么可以通过 this 直接访问 data 中的数据:

new Vue({
  el'#app',
  data: {
    message'Hello, Vue!'
  },
  methods: {
    showMessage() {
      console.log(this.message);
    }
  }
});

在这个示例中,message 属性被定义在 data 中。在 methods 中的 showMessage 方法中,可以直接通过 this.message 访问到 data 中的 message 属性。

这种代理机制让代码更加简洁,提高了代码的可读性和维护性。需要注意的是,data 中的属性必须是在实例化时就已经存在的,如果后续添加新的属性,需要使用 Vue 提供的方法进行添加,并且这些新增属性不会被自动代理到实例上。

最后

给我们的辅导服务打个广告。