Vue 根组件的挂载有两种方式,一种是在根组件的 options 中配置 el,第二种是在根组件实例化后调用 $mount,两者是冲突的,有了前者,后者便不会生效。当然前者在初始化后调用的也还是 $mount。
let me see see $mount
仅讨论 web 情况
// src\core\instance\init.js =======================================================================================
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
// src\platforms\web\runtime\index.js ==============================================================================
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
// 入参及环境的校验,获取到 el 对应的 DOM,否则则是 undefined
el = el && inBrowser ? query(el) : undefined
return mountComponent(this, el, hydrating)
}
// src\platforms\web\entry-runtime-with-compiler.js ================================================================
// 缓存原先的 $mount 函数,也就是上面运行时的 $mount,在最后还是会调用的
const mount = Vue.prototype.$mount
// 核心目的是得到一个有效的 render 函数
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
// 获取对应 DOM
el = el && query(el)
// 如果 DOM 是 body 或者 body 则非法,抛出错误
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
}
// 获取初始化之后的 options
const options = this.$options
// resolve template/el and convert to render function
// 如果未编写 render 函数,如果没有,则用各种内容尝试得到一个有效的 template 来使用
if (!options.render) {
let template = options.template
// 如果有 template
if (template) {
if (typeof template === 'string') {
// 如果 template 是 string,且不是已 '#' 号开头的,则直接将其当做 template
// 如果 template 是 string 且已 '#' 号为开头,则视为 css 选择器,然后找到对应 DOM 的 innerHTML 为 template
// 如果对应的 innerHTML 为空则抛出非法报错
if (template.charAt(0) === '#') {
template = idToTemplate(template)
if (process.env.NODE_ENV !== 'production' && !template) {
warn(
`Template element not found or is empty: ${options.template}`,
this
)
}
}
} else if (template.nodeType) {
// 如果 template 直接是个 DOM 了,则直接取其 innerHTML
template = template.innerHTML
} else {
// 如果以上条件均不成立,就是既没有 render, template 也不是 string 或者 DOM,说明 template 是个非法的内容,则直接报错结束
if (process.env.NODE_ENV !== 'production') {
warn('invalid template option:' + template, this)
}
return this
}
} else if (el) {
// 如果既没有 render 也没有 template,则将挂载的 DOM 的 outerHTML 为 template
template = getOuterHTML(el)
}
// 如果存在 template,一般经过上面的过滤应该存在了
if (template) {
// 性能计算,pass
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
mark('compile')
}
// 将 template 转为 render 函数赋值 options,返回
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
// 性能计算,pass
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
mark('compile end')
measure(`vue ${this._name} compile`, 'compile', 'compile end')
}
}
}
// 调用原生的 $mount
return mount.call(this, el, hydrating)
}
mountComponent
// src\core\instance\lifecycle.js
export function mountComponent (
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
// this.$el 存储的是挂载的 DOM
vm.$el = el
// 如果没有 render 函数,
// 没有 render 函数有两种情况
// 一是,仅用的 runtime 包,却没有写 render
// 二是,用了 runtime-with-compiler 包,但是没有有效的 el, template, render 的情况,才会没有 render
// 下面均是因为上面的两种都属于非法行为而抛出的错误
if (!vm.$options.render) {
// 先初始化一个空的 VNode
vm.$options.render = createEmptyVNode
if (process.env.NODE_ENV !== 'production') {
/* istanbul ignore if */
// 如果写了纯 template 或者有 el,则告诉开发者,当前的开发环境是没有 compile 包的, 用 template 是无效的
if ((vm.$options.template && vm.$options.template.charAt(0) !== '#') ||
vm.$options.el || el) {
warn(
'You are using the runtime-only build of Vue where the template ' +
'compiler is not available. Either pre-compile the templates into ' +
'render functions, or use the compiler-included build.',
vm
)
} else {
// 如果没有,render,el,template,则抛出错误,提示需要有效的配置
warn(
'Failed to mount component: template or render function not defined.',
vm
)
}
}
}
// 触发 beforeMount 生命周期函数
callHook(vm, 'beforeMount')
// 开始挂载
// updateComponent 把渲染函数生成的虚拟DOM渲染成真正的DOM
let updateComponent
// if-else 实际没差,前者只是拆分了后者运行,并加了一些性能分析
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
updateComponent = () => {
const name = vm._name
const id = vm._uid
const startTag = `vue-perf-start:${id}`
const endTag = `vue-perf-end:${id}`
mark(startTag)
const vnode = vm._render()
mark(endTag)
measure(`vue ${name} render`, startTag, endTag)
mark(startTag)
vm._update(vnode, hydrating)
mark(endTag)
measure(`vue ${name} patch`, startTag, endTag)
}
} else {
updateComponent = () => {
// 1. vm._render() 将 render 变成虚拟 DOM
// 2. vm._update() 将 虚拟 DOM 变成真实 DOM
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 用于每次数据发生变化时的前置调用,正好与 beforeUpdate 的意图吻合,当然前提是实例已经被挂载,同时未被销毁
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
}