我们在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');
面对这简单的几行代码,在好奇心的驱使下,我们可能会问这样几个问题:
- 这里通过import导入的Vue从哪里来?
- 这里导入的组件App是个什么东西?
- #app在这里起到了什么作用?
- 为什么,仅仅是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