浅析Vue初始化过程(基于Vue2.6)

2022-09-27 14:11:49 浏览数 (1)

我们在new Vue对象的时候,其实就对Vue进行了一个初始化的过程。鉴于new Vue背后发生的事情太多,难以用一篇文章就概述完全,因此本文侧重于从整体流程上来分析,至于各大分支逻辑,笔者将在后续的文章中一一进行解析

我们在通过vue-cli初始化一个vue项目之后,经常看见类似如下的代码:

代码语言:javascript复制
import Vue from 'vue'
import App from './App.vue'

new Vue({
  render: h => h(App)
}).$mount('#app');

面对这简单的几行代码,在好奇心的驱使下,我们可能会问这样几个问题:

  1. 这里通过import导入的Vue从哪里来?
  2. 这里导入的组件App是个什么东西?
  3. #app在这里起到了什么作用?
  4. 为什么,仅仅是new了一个Vue实例,App.vue的内容就能渲染到页面上?new Vue背后到底做了些什么事情?

那接下来,本文就从上面四个方面,来逐一进行分析。

一、import导入的Vue从哪里来?

要回答这个问题,我们现将视线放到Vue源码中package.json文件中,有这么两行代码:

代码语言:javascript复制
  "main": "dist/vue.runtime.common.js",
  "module": "dist/vue.runtime.esm.js"

正常情况下,我们执行import Vue from 'vue'的时候,引入的就是main或者module所对应的js文件,原因如下:

  • main : 定义了 npm 包的入口文件,browser 环境和 node 环境均可使用
  • module : 定义 npm 包的 ESM 规范的入口文件,browser 环境和 node 环境均可使用

但是,如果我们用webpack构建项目,而webpack可以设置别名,比如:

代码语言:javascript复制
{
  vue: resolve('src/platforms/web/entry-runtime-with-compiler')
}

也就是说,在webpack的配置文件中,别名vue所在的文件,才是我们在执行import Vue from 'vue'的时候所真正引入的文件。那么这里的构造函数Vue就是其所对应的文件中导出来的。

二、这里导入的组件App是个什么东西?

大家平时在编写vue项目的时候经常会编写名为 xxx.vue的文件,文件中包含了template、script、style等信息,而这样的信息,浏览器其实是无法识别的,因为浏览器只能识别正常的javascript、普通的html,而template这种东西都是在vue这个体系下提供的能力,浏览器并不能直接识别。那浏览器既然不能识别,就得把我们所编写的代码转化成可以被识别的东西。那谁来转化呢,当然只能由Vue来转化。然而Vue只是一个构造函数,真正来控制这一切的只能是Vue的实例,也就是我们标题提到的new Vue({})。其实在源码中可以看到,组件在本质上就是一个Vue实例。而我们在文章开始的时候看到的那段代码,可以看作是初始化了整个页面中最大的组件。虽然Vue实例和组件实例在源码上有些区别,但非常相似。

三、#app在这里起到了什么作用?

#app其实就是vue 实例将要把自己所控制的组件App转化成真实的DOM后,这个真实DOM的最终归宿——替换页面上id为app的DOM。

四、new Vue到底发生了什么?

new Vue发生了什么,我说了不算,得看Vue是个什么,代码如下:

代码语言:javascript复制
//src/core/instance/index.js

import { initMixin } from './init'
import { stateMixin } from './state'
import { renderMixin } from './render'
import { eventsMixin } from './events'
import { lifecycleMixin } from './lifecycle'
import { warn } from '../util/index'

function Vue (options) {
  if (process.env.NODE_ENV !== 'production' &&
    !(this instanceof Vue)
  ) {
    warn('Vue is a constructor and should be called with the `new` keyword')
  }
  /**
   * 这是Vue进行初始化的真正开端
   */
  this._init(options)
}

/**
 * 同src/core/index.js中的代码类似,下面五个方法,同样也为Vue赋予了不同的能力
 */
/**
 * Vue.prototype._init = function ...
 **/
initMixin(Vue)
/**
 * Vue.prototype.'$data'
 * Vue.prototype.'$props'
 * Vue.prototype.$set
 * Vue.prototype.$delete
 * Vue.prototype.$watch
 */
stateMixin(Vue)
/**
 * Vue.prototype.$on
 * Vue.prototype.$once
 * Vue.prototype.$off
 * Vue.prototype.$emit
 */
eventsMixin(Vue)
/**
 * Vue.prototype._update
 * Vue.prototype.$forceUpdate
 * Vue.prototype.$destroy
 */
lifecycleMixin(Vue)
/**
 * Vue.prototype.$nextTick
 * Vue.prototype._render
 * Vue.prototype._o = markOnce
 * Vue.prototype._n = toNumber
 * Vue.prototype._s = toString
 * Vue.prototype._l = renderList
 * Vue.prototype._t = renderSlot
 * Vue.prototype._q = looseEqual
 * Vue.prototype._i = looseIndexOf
 * Vue.prototype._m = renderStatic
 * Vue.prototype._f = resolveFilter
 * Vue.prototype._k = checkKeyCodes
 * Vue.prototype._b = bindObjectProps
 * Vue.prototype._v = createTextVNode
 * Vue.prototype._e = createEmptyVNode
 * Vue.prototype._u = resolveScopedSlots
 * Vue.prototype._g = bindObjectListeners
 * Vue.prototype._d = bindDynamicKeys
 * Vue.prototype._p = prependModifier
 */
renderMixin(Vue)

export default Vue

那这个_init方法又在哪里定义的呢?其实上面代码片段中的注释已经回答了这个问题。当程序执行了initMixin(Vue)后,Vue.prototype._init = function ...

,好了,我们去看看这个init方法都干了什么:

代码语言:javascript复制
//src/core/instance/init.js
Vue.prototype._init = function (options?: Object) {
    const vm: Component = this;
    // a uid
    vm._uid = uid  ;

    let startTag, endTag;
    /* istanbul ignore if */
    if (process.env.NODE_ENV !== "production" && config.performance && mark) {
      startTag = `vue-perf-start:${vm._uid}`;
      endTag = `vue-perf-end:${vm._uid}`;
      mark(startTag);
    }

    // a flag to avoid this being observed
    vm._isVue = true;
    // merge options
    if (options && options._isComponent) {
      // optimize internal component instantiation
      // since dynamic options merging is pretty slow, and none of the
      // internal component options needs special treatment.
      initInternalComponent(vm, options);
    } else {
      vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor), //得到构造函数以及构造函数的父构造函数所有的options
        options || {},
        vm
      );
    }
    /* istanbul ignore else */
    if (process.env.NODE_ENV !== "production") {
      initProxy(vm);
    } else {
      vm._renderProxy = vm;
    }
    // expose real self
    vm._self = vm;
    initLifecycle(vm);
    initEvents(vm);
    initRender(vm);
    callHook(vm, "beforeCreate");
    initInjections(vm); // resolve injections before data/props
    initState(vm);
    initProvide(vm); // resolve provide after data/props
    callHook(vm, "created");

    /* istanbul ignore if */
    if (process.env.NODE_ENV !== "production" && 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),本质上,就是把vm控制的VNode,挂载到vm关联的DOM上面,这里的vm.$options.el就是这个目标DOM
       * 可能会觉得奇怪,前面src/core/index.js 和 src/core/instance/index.js两个文件中,并没有涉及到$mount方法,这个vm怎么突然钻出来了个$mount方法,
       * 实际上,在程序执行到src/core/index.js之前,会有一个入口文件,这个入口文件在src/platforms/web/entry-runtime-with-compiler.js
       * 其中src/platforms/web/entry-runtime-with-compiler.js里面会复用src/platforms/web/entry-runtime.js文件中的Vue.prototype.$mount方法,也就是说
       * Vue.prototype.$mount在程序开始之初就已经存在了
       */
      vm.$mount(vm.$options.el);
    }
  };
}

看起来这个init方法很长,但是请大家莫慌,其实这里可以概括为做了三件事:

  • 执行mergeOptions方法,因为本文开始的示例不会走组件Options合并的逻辑,所以暂时不提initInternalComponent这个方法。
  • 对生命周期、事件中心、Render、injection、state、provide等进行初始化
  • 执行vm.$mount(vm.options.el);

这里提到的第一件事,其实把父构造函数上挂载的能力叠加到本实例上,首次初始化并不涉及父构造函数的父构造函数这种复杂的情况。

这里提到的第二件事,目前可以简单理解为往vue实例上添加了各种能力,至于这些能力的细节我们后续的文章中讨论,本文只聊主体流程。

这里提到的第三件事,vm.$mount(vm.options.el),是关键。首先我们得知道这里的$mount方法,vm什么时候有的这样一个方法。我们可以这样理解,一开始Vue构造函数是光秃秃的,后来随着初始化,有了各种属性各种方法。那这里的$mount方法,何时拥有的呢?答案是,在入口文件中,入口文件在哪?

代码语言:javascript复制
//src/platform/web/entry-runtime-with-compiler.js
const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && query(el)

  /* istanbul ignore if */
  if (el === document.body || el === document.documentElement) {
    process.env.NODE_ENV !== 'production' && warn(
      `Do not mount Vue to <html> or <body> - mount to normal elements instead.`
    )
    return this
  }
  
  const options = this.$options
  // resolve template/el and convert to render function
  if (!options.render) {
    let template = options.template
    if (template) {
      if (typeof template === 'string') {
        if (template.charAt(0) === '#') {
          template = idToTemplate(template)
          /* istanbul ignore if */
          if (process.env.NODE_ENV !== 'production' && !template) {
            warn(
              `Template element not found or is empty: ${options.template}`,
              this
            )
          }
        }
      } else if (template.nodeType) {
        template = template.innerHTML
      } else {
        if (process.env.NODE_ENV !== 'production') {
          warn('invalid template option:'   template, this)
        }
        return this
      }
    } else if (el) {
      template = getOuterHTML(el)
    }
    if (template) {
      /* istanbul ignore if */
      if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
        mark('compile')
      }

      const { render, staticRenderFns } = compileToFunctions(template, {
        outputSourceRange: process.env.NODE_ENV !== 'production',
        shouldDecodeNewlines,
        shouldDecodeNewlinesForHref,
        delimiters: options.delimiters,
        comments: options.comments
      }, this)
      options.render = render
      options.staticRenderFns = staticRenderFns

      /* istanbul ignore if */
      if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
        mark('compile end')
        measure(`vue ${this._name} compile`, 'compile', 'compile end')
      }
    }
  }
  return mount.call(this, el, hydrating)
}

其实我们看了这段代码会发现,这个mount方法,只不过是在this.options挂载了一个render函数。最后一行代码 mount.call(this, el, hydrating),这里的mount实际上在 src/platform/web/runtime/index.js中:

代码语言:javascript复制
//src/platform/web/runtime/index.js
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && inBrowser ? query(el) : undefined
  return mountComponent(this, el, hydrating)
}

受限于篇幅,本文先到这里,下一篇文章笔者将会探析mount方法中发生的一些有趣的事情。今天大家只需要知道下面几点就算达成目标:Vue在初始化过程中,首先是通过在Vue.prototype上以及Vue构造函数自身上不断的添加函数和属性,为其赋能。赋予相应能力后再执行mount方法,在mount方法执行过程中,会想办法把vue实例所控制的组件等内容转化成DOM并挂载到向mount传入的参数DOM节点上。欢迎大家多多交流。

欢迎关注github,内有笔者不断完善的源码注释:

https://github.com/creator-yangyitao/vue2.6-source-code-analyse

0 人点赞