前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >浅析Vue初始化过程(基于Vue2.6)

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

作者头像
杨艺韬
发布2022-09-27 14:11:49
5150
发布2022-09-27 14:11:49
举报
文章被收录于专栏:前端框架源码剖析

我们在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

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2021-04-04,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 杨艺韬 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
事件总线
腾讯云事件总线(EventBridge)是一款安全,稳定,高效的云上事件连接器,作为流数据和事件的自动收集、处理、分发管道,通过可视化的配置,实现事件源(例如:Kafka,审计,数据库等)和目标对象(例如:CLS,SCF等)的快速连接,当前 EventBridge 已接入 100+ 云上服务,助力分布式事件驱动架构的快速构建。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档