本文主要是对$mount
方法进行分析,下面以entry-runtime-with-compiler
这个版本来对这个方法进行分析。
下面是entry-runtime-with-compiler
版本对$mount
方法重写的部分。
// 缓存本身的mount方法
const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
// 通过选择器选择挂载元素
el = el && query(el)
// 限制挂载的dom元素
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
// 检查是否有render函数,这里为了确保this.options上有render函数
if (!options.render) {
let template = options.template
// 这里的template是我们传入的模板
if (template) {
if (typeof template === 'string') {
// 如果值以 # 开始,则它将被用作选择符,并使用匹配元素的 innerHTML 作为模板。
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) {
// 处理 传入DOM的情况
template = template.innerHTML
} else {
if (process.env.NODE_ENV !== 'production') {
warn('invalid template option:' + template, this)
}
return this
}
} else if (el) {
// 实在没有template,根据el进行获取
// outerHTML 内容包含描述元素及其后代的序列化HTML片段,即当前元素及后代元素的HTML字符串
template = getOuterHTML(el)
}
if (template) {
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
mark('compile')
}
// 通过compileToFunctions方法根据template生成render方法
const { render, staticRenderFns } = compileToFunctions(template, {
outputSourceRange: process.env.NODE_ENV !== 'production',
shouldDecodeNewlines,
shouldDecodeNewlinesForHref,
delimiters: options.delimiters,
comments: options.comments
}, this)
// 将render挂到$options上
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')
}
}
}
// 调用缓存的公用mount方法
return mount.call(this, el, hydrating)
}
主要的逻辑在代码中进行了注释,该版本的代码最终也是调用公共的$mount
方法,进行重写的目的是确保$options
上有render
方法,处理逻辑根据不同的el
和template
进行render
函数的生成。通过降级处理,保证template
的存在,再通过template
创建render
函数。降级依次是
- 用传入的
template
- 是字符串,如果首字母是
#
,识别为选择器,使用对应dom元素的innerHtml
作为template
,否则直接使用该字符串 - 是dom元素,,使用该元素的
innerHTML
, - 其他直接警告并终止处理
- 是字符串,如果首字母是
template
不存在,使用传入的el
的outerHTML
其实这里也解释了compiler
版本的含义,entry-runtime-with-compiler
和entry-runtime
区别在于这段$mount
的重写,官方对两者差异解释在这里。在使用脚手架时我们实际是使用的runtime
版本,那我们也没有人为传入render
函数,也只是写了模板,填入Vue
的选项而已。其实这一步脚手架或者webpack
帮我们做了,当使用 vue-loader
或 vueify
的时候,*.vue
文件内部的模板会在构建时预编译成 JavaScript。
下面我们来看下运行时和编译器+运行时两个版本都在使用的原味$mount
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && inBrowser ? query(el) : undefined
return mountComponent(this, el, hydrating)
}
实际是返回mountComponent
函数调用的返回值,以下是mountComponent
的精简代码。
export function mountComponent (
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
vm.$el = el
// 如果还是没有render函数,以创建空VNode函数作为render函数
if (!vm.$options.render) {
vm.$options.render = createEmptyVNode
}
callHook(vm, 'beforeMount')
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
new Watcher(vm, updateComponent, noop, {
// 组件更新情况下,调用 beforeUpdate HOOK
before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
if (vm.$vnode == null) {
vm._isMounted = true
callHook(vm, 'mounted')
}
return vm
}
主要步骤是生成渲染式Watcher
,关于渲染式Watcher
在渲染式Watcher工作流程中有简单介绍。我们要知道初始化时这里会去执行vm._update
。
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
const vm: Component = this
const prevEl = vm.$el
const prevVnode = vm._vnode
const restoreActiveInstance = setActiveInstance(vm)
vm._vnode = vnode
// Vue.prototype.__patch__ is injected in entry points
// based on the rendering backend used.
// 重点在这里,根据prevVnode是否存在决定是初始化还是更新
if (!prevVnode) {
// initial render
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
} else {
// updates
vm.$el = vm.__patch__(prevVnode, vnode)
}
restoreActiveInstance()
// update __vue__ reference
if (prevEl) {
prevEl.__vue__ = null
}
if (vm.$el) {
vm.$el.__vue__ = vm
}
// if parent is an HOC, update its $el as well
if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {
vm.$parent.$el = vm.$el
}
// updated hook is called by the scheduler to ensure that children are
// updated in a parent's updated hook.
}
这里主要是执行__patch__
函数来进行VNode
到真正DOM
节点的转换。再啰嗦两句,对于_update
我们的调用方式是vm._update(vm._render(), hydrating)
,vm._update
的接收参数第一个是VNode
,由此我们可以推断出来vm.render()
返回的是VNode
。实际上也确实是这样,在前面分析compiler
版本时,我们看到 render
函数的生成来自于el
和template
。所以这里可以得出一个流程是
template模板 --> render函数 --> VNode --> 真正DOM
render
是编译template
到VNode
的关键,__patch__
是实现最后VNode
到真实DOM
转换的关键,这两个函数是我们后面分析的重点。
$mount
的目的就是将我们编写的模板变为真实DOM,并挂载到指定的元素上去,其中经历了上述的转换过程。我们先来看看从模板到VNode的render
函数是怎样的。