深入解读vue源码(五)挂载实例的实现

基本介绍

vue中我们是通过$mount方法去实现vm的挂载,这个方法在源码中很多文件都有定义如:

  • src/platform/web/entry-runtime-with-compiler.js
  • src/platform/web/runtime/index.js
  • src/platform/weex/runtime/index.js

其实在前面的几片文章的介绍中我们可以看出来这些文件就是对不同平台、不同环境、不同的构建方式进行的区分,在这篇文章中会介绍带有compiler版本的$mount方法的实现原理。


compiler版本$mount的实现

简单回顾

首先我们先回顾下,在上篇文章中我们讲解了new Vue的实现过程,在src/core/instance/index.js文件下能看到这样的代码。

1
2
3
import { initMixin } from './init'
...
initMixin(Vue)

通过点击能够查看到initMixin方法,在这个方法中最底部有这样一段代码。

1
2
3
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}

这段代码是在el不为空的时候将其通过$mount方法挂载到dom中。

$mount的实现

compiler版本$mount的实现在src/platform/web/entry-runtime-with-compiler.js文件,打开这个文件我们一步步进行分析。

1
const mount = Vue.prototype.$mount

这句话是将原型链上的$mount方法缓存在mount上。

接着我们深入去看看$mount这个方法是通过什么方式定义的,这个方法定义的路径在src/platforms/web/runtime/index.js下,该文件中有这样一段代码。

1
2
3
4
5
6
7
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && inBrowser ? query(el) : undefined
return mountComponent(this, el, hydrating)
}

回归正轨,我们继续分析这个src/platform/web/entry-runtime-with-compiler.js文件。

1
2
3
4
5
6
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
...
}

这段代码重新定义了$mount在原型链上的方法,这时候很多人会好奇,为什么会定义两遍。

其实src/platform/web/entry-runtime-with-compiler.js文件中的$mount方法是一个重写复用的方法,这个方法只在compiler版本中存在,在runtime-only版本中并不存在。

在这里我们能够看到这个方法接收两个参数,一个参数是el,它表示挂载的元素,可以是字符串类型,也可以是DOM对象,第二个参数hydrating是和服务端渲染相关的,在客户端浏览器渲染的情况下我们可以忽略这个参数。

在这个方法内部,我们会看到一个query方法。

1
el = el && query(el)

query方法是在src/platforms/web/util/index.js这个文件中进行定义的,这个方法也是比较简单的,总的来说就是调用原生的document.querySelector方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
export function query (el: string | Element): Element {
if (typeof el === 'string') {
const selected = document.querySelector(el)
if (!selected) {
process.env.NODE_ENV !== 'production' && warn(
'Cannot find element: ' + el
)
return document.createElement('div')
}
return selected
} else {
return el
}
}

这个方法通过判断传入的el是否是string类型,如果是的话将其转换成dom元素保存在selected中,并且判断selected是否为空,为空则警告,不为空则query方法返回selected,当传入的el是元素节点类型则直接返回。

1
2
3
4
5
6
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
}

继续分析$mount方法,能够看到这样一段代码,这段代码的作用就是判断el的标签中有bodyhtml标签的情况下进行警告,这个警告的原因是mount方法是通过覆盖的形式进行挂载的,所以不能直接挂载到这两个标签上,通常情况下我们的挂载都是通过id进行挂载的。

1
2
3
if (!options.render) {
...
}

继续往下看代码,会看到对render的处理,通过判断是否有render这个函数,我们在使用vue-cli创建一个项目的时候在main.js文件中会看到这样的代码,这段代码中在new Vue的过程中创建了一个render并且挂载到了#app下。

1
2
3
4
5
new Vue({
router,
store,
render: function (h) { return h(App) }
}).$mount('#app')

在判断render的内部又进行了对template的判断,值得注意的是,如果想使用template就要使用compiler版本。这render的内部有两个对template的判断,首先我们来分析第一个。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
let template = options.template // 拿到template定义的值
if (template) {
if (typeof template === 'string') { // 判断template为string类型
if (template.charAt(0) === '#') { // 判断template中的第一个字符为#号
template = idToTemplate(template)
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && !template) { // 判断template是否为空,为空的情况下提示找不到模板元素或模板元素为空
warn(
`Template element not found or is empty: ${options.template}`,
this
)
}
}
} else if (template.nodeType) { // 判断template为节点类型
template = template.innerHTML
} else {
if (process.env.NODE_ENV !== 'production') { // 不是这string类型或节点类型的情况下提示无效的模板选项
warn('invalid template option:' + template, this)
}
return this
}
} else if (el) {
template = getOuterHTML(el) // template是字符串类型
}

这段代码中包含了两个方法,分别是idToTemplategetOuterHTML。在这里先简单介绍下idToTemplate方法。

1
2
3
4
const idToTemplate = cached(id => {
const el = query(id)
return el && el.innerHTML
})

这段代码将template传入到query方法,在之前我们看到了query方法中接收的两个参数类型,一个是string,一个是Element,对于idToTemplate方法,就是将Element类型传入query方法。

接下来我们继续看getOuterHTML这个方法。

1
2
3
4
5
6
7
8
9
function getOuterHTML (el: Element): string {
if (el.outerHTML) {
return el.outerHTML
} else {
const container = document.createElement('div')
container.appendChild(el.cloneNode(true))
return container.innerHTML
}
}

这个方法是判断el中是否有后代元素并且是否只包含一个父级的DOM节点,如果是直接返回,如果不是则在外层包一个div,使其只有一个父级节点。

render的内部对template的二个判断是跟编译相关的,在后面的章节中我们还会进行详细的介绍,在这里我们只需要知道$mount方法就是判断是否有render函数,有的话直接使用,没有的话通过编译template使其自动生成个render函数,也可以理解为我们在使用vue进行任何业务功能的编写时,都是依赖render函数的挂载得以实现的。

$mount这个原型链方法的最后还有这样一段代码。

1
return mount.call(this, el, hydrating)

这段代码中的mount方法是文件最开始写入缓存的$mount方法,所以这段代码本质依旧是调用原型链上的方法,这个方法在文章的最开始已经介绍过了,是在src/platforms/web/runtime/index.js下的方法。

在这个$mount方法内,能看到他最后返回的是一个名字叫做mountComponent的函数,接下来我们去看一下这个函数,这个函数定义在src/core/instance/lifecycle.js这里。

1
2
3
4
5
6
7
8
export function mountComponent (
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
vm.$el = el
...
}

首先这里会将el缓存到vm中。

继续往下分析,我们会看到这样一段代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
if (!vm.$options.render) {
vm.$options.render = createEmptyVNode
if (process.env.NODE_ENV !== 'production') {
/* istanbul ignore if */
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 {
warn(
'Failed to mount component: template or render function not defined.',
vm
)
}
}
}

这段代码判断的是是否有render,没有render的情况下会给render赋值一个空的节点,并且在不是生产环境的情况下抛出一个警告信息,为什么会存在没有render的情况呢,有些时候我们使用了runtime-only版本,并且定义了template,这个时候我们的runtime-only版本中并没有对$mount方法进行重写,所以无法获取一个render

这个方法中还有一些信息,我们在这里只做一些简单的介绍

1
2
3
4
5
6
7
8
9
10
callHook(vm, 'beforeMount') // 生命周期相关,在后续的文章中会单独进行介绍
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
updateComponent = () => { // vue提供的性能埋点系统,用以监控程序运行状态
...
}
} else {
updateComponent = () => {
vm._update(vm._render(), hydrating) // vNode
}
}

这段代码中与性能埋点系统相关的东西,不在我的介绍范围内,详细的可以去官网查看有关文档,我们这里要去分析updateComponent方法,这个方法是怎么执行的呢。

1
2
3
4
5
6
7
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)

这里通过vm._render方法生成一个虚拟DOM,在实例化一个Watcher方法,在调用updateComponent方法。

最终调用VM._update方法更新DOMWatcher方法在这里有两个作用,一是用来初始化时候执行回调函数,二是在vm实例中监测的数据发生改变时触发回调函数。

1
2
3
4
5
if (vm.$vnode == null) {
vm._isMounted = true
callHook(vm, 'mounted')
}
return vm

函数在最后判断跟节点的时候设置为true,表示这个实例已经挂载了,紧跟着执行mounted生命周期。

这里值得注意的是vm.$vnode == null表示vue实例的父级的虚拟节点,所以为null时候表示根vue实例。


总结

这篇文章主要讲解了$mountmountComponent方法,这两个方法的逻辑也是十分清晰,它完成了整个实例化渲染DOM的功能。


相关链接

------本文结束,感谢您的阅读,如有问题请通过邮件方式联系作者------