Vue实例挂载的实现

Vue实例挂载的实现

Vue中我们通过\$mount实例方法去挂载vm的,\$mount方法在多个文件中都有定义,如src/platform/web/entry-runtime-with-compiler.jssrc/platform/web/runtime/index.jssrc/platform/weex/runtime/index.js因为\$mount这个方法的实现是和平台、构建方式都相关的。接下来我们重点分析compiler版本的\$mount实现,因为抛开webpack的vue-loader,我们在纯前端浏览器环境分析Vue的工作原理,有助于我们对原理理解的深入。

compiler版本的\$mount实现很有意思,先来看一下src/platform/web/entry-runtime-with-compiler.js文件中定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
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.`
)
rerurn 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}`)
}
}
}
}
}
}

这段代码首先缓存了原型上的\$mount方法,再重新定义该方法。首先,它对el做了限制,Vue不能挂载在body、html这样的根节点上。接下来的是很关键的逻辑–如果没有定义render方法,则会把el或者template字符串转换成render方法。这里我们要牢记,在Vue2.0版本中,所有Vue的组件的渲染最终都需要render方法,无论我们是用单文件.vue方式开发组件,还是写了el或者template属性,最终都会转换成render方法,那么这个过程是 Vue 的一个“在线编译”的过程,它是调用 compileToFunctions 方法实现的,编译过程我们之后会介绍。最后,调用原先原型上的 \$mount 方法挂载。

原先原型上的\$mount方法在src/platform/web/runtime/index.js 中定义,之所以这么设计完全是为了复用,因为它是可以被 runtime only 版本的 Vue 直接使用的。

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

\$mount 方法实际上会去调用mountComponent方法,这个方法定义在src/core/instance/lifecycle.js文件中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
export function mountComponent (
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
vm.$el = el
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)
}
}
}
callHook(vm, 'beforeMount')
}

从上面的代码可以看到,mountComponent核心就是先调用vm._render方法先生成虚拟Node,再实例化一个渲染Watcher,在它的回调函数中会调用updateComponent方法,最终调用vm._update更新DOM。

Watcher在这里起到两个作用,一个是初始化的时候会执行回调函数,另一个是当vm实例中的监测的数据发生变化的时候执行回调函数。

函数最后判断为根节点的时候设置 vm._isMountedtrue, 表示这个实例已经挂载了,同时执行 mounted 钩子函数。 这里注意 vm.$vnode 表示 Vue 实例的父虚拟 Node,所以它为 Null 则表示当前是根 Vue 的实例。

总结

mountComponent 方法的逻辑也是非常清晰的,它会完成整个渲染工作,接下来就是分析其中的细节,也就是最核心的2个方法:vm._rendervm._update