这是我参与更文挑战的第14天,活动详情查看: 更文挑战
前言
写过Vue代码的人都知道,我们在平时创建一个Vue实例的时候有两种写法:
第一种:实例化一个Vue对象
new Vue({
el: '#app',
data: {
message: 'Hello Vue!'
}
})
第二种:实例化组件
import App from './App.vue';
new Vue({
router,
store,
render: h => h(App)
}).$mount('#app');
无论哪种方式,都调用了Vue的构造函数,那这个new Vue的具体过程到底做了什么呢??
Vue初始化
Vue的构造函数定义在src/core/instance/index.js中,它主要是调用了一个_init的方法
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')
}
this._init(options)
}
_init方法定义在src/core/instance/init.js中
Vue.prototype._init = function (options?: Object) {
const vm: Component = this
// a uid
vm._uid = uid++
let startTag, endTag
...
// 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 || {},
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')
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
当Vue实例是组件时,执行initInternalComponent方法;否则执行mergeOptions
initInternalComponent
该方法整体来看就是给vm.$options上添加属性,具体的实例化组件后边专门说。
export function initInternalComponent (vm: Component, options: InternalComponentOptions) {
const opts = vm.$options = Object.create(vm.constructor.options)
// doing this because it's faster than dynamic enumeration.
const parentVnode = options._parentVnode
opts.parent = options.parent
opts._parentVnode = parentVnode
const vnodeComponentOptions = parentVnode.componentOptions
opts.propsData = vnodeComponentOptions.propsData
opts._parentListeners = vnodeComponentOptions.listeners
opts._renderChildren = vnodeComponentOptions.children
opts._componentTag = vnodeComponentOptions.tag
if (options.render) {
opts.render = options.render
opts.staticRenderFns = options.staticRenderFns
}
}
mergeOptions
看代码mergeOptions主要是将parent和child的options配置参数合并,在这里parent就是构造函数的options,child是实例化的时候传入的options。当child._base为true时,定义在extends和mixins中的也会被合并。
defaultStrat与strats两个变量包含了具体的合并策略,这里先不讲,后边专门开一章说这块。
export function mergeOptions (
parent: Object,
child: Object,
vm?: Component
): Object {
if (process.env.NODE_ENV !== 'production') {
checkComponents(child)
}
if (typeof child === 'function') {
child = child.options
}
normalizeProps(child, vm)
normalizeInject(child, vm)
normalizeDirectives(child)
// Apply extends and mixins on the child options,
// but only if it is a raw options object that isn't
// the result of another mergeOptions call.
// Only merged options has the _base property.
if (!child._base) {
if (child.extends) {
parent = mergeOptions(parent, child.extends, vm)
}
if (child.mixins) {
for (let i = 0, l = child.mixins.length; i < l; i++) {
parent = mergeOptions(parent, child.mixins[i], vm)
}
}
}
const options = {}
let key
for (key in parent) {
mergeField(key)
}
for (key in child) {
if (!hasOwn(parent, key)) {
mergeField(key)
}
}
function mergeField (key) {
const strat = strats[key] || defaultStrat
options[key] = strat(parent[key], child[key], vm, key)
}
return options
}
几个初始化函数在这里也先不具体展开,到后边会每个展开说。
_init方法主要就是初始化vm,初始化时会依次执行的生命周期钩子beforeCreate,created。初始化完事件,data等。
接下来会判断$options中是否传了el,如果传了就是我们的第一种new Vue的方式,则直接调用了vm.$mount(vm.$options.el);如果没传el,我们会手动调用$mount('#app')。其实无论实例化Vue组件还是实例化Vue对象,我们都可以传el参数过去,当然也可以选择手动调用$mount
看一下官方这个图:
可以看出无论是否传el参数,最后都是调用了$mount方法,接下来看下这个方法的实现
Vue挂载实例
从官方生命周期图可以看出,调用$mount就意味着要进入模板编译与挂载阶段了。
Vue是通过$mount实例方法挂载vm的,$mount方法有多个定义,因为它的实现与平台以及构建方式有关,所以会有多个实现方式,但都是通过在原型上定义的mount方法去扩展的。
至于template是怎么转化成render函数的,后边专门来看。这里先看一下Vue原型上定义的$mount方法: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方法主要接收两个参数:第一个是挂载的元素(可以字符串,也可以DOM对象),第二个是服务端渲染参数。最后真正是调用了mountComponent方法:src/core/instance/lifecycle.js
看一下mountComponent方法的重点部分:
export function mountComponent (
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
vm.$el = el
if (!vm.$options.render) {
vm.$options.render = createEmptyVNode
...
}
callHook(vm, 'beforeMount')
let updateComponent
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
updateComponent = () => {
...
}
} else {
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
}
// we set this to vm._watcher inside the watcher's constructor
// since the watcher's initial patch may call $forceUpdate (e.g. inside child
// component's mounted hook), which relies on vm._watcher being already defined
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
hydrating = false
// manually mounted instance, call mounted on self
// mounted is called for render-created child components in its inserted hook
if (vm.$vnode == null) {
vm._isMounted = true
callHook(vm, 'mounted')
}
return vm
}
mountComponet首先会调用beforeMount钩子。然后定义了updateComponent
接下来实例化一个渲染Watcher,这也是mountComponent核心。该Watcher的作用:在new Vue实例的时候执行回调函数(updateComponent);vm实例中的监测数据发生变化时执行回调函数(具体watcher实现后边补充)
vm.$vnode表示Vue实例的父虚拟节点,它为null,则表示当前是根Vue的实例。
updateComponent回调函数的定义很简单:vm._update(vm._render(), hydrating),主要调用了两个方法:_render和_update,_render是把Vue实例渲染成一个Vnode,_update是将Vnode渲染成真实DOM
总结
Vue项目的入口文件在new Vue实例的过程中主要做了两件事,一个是初始化vm(各种事件,参数等),一个是挂载Vue实例到'#app'上