往期章节
前面四章中,我们跟随 Vue.js 技术揭秘 , 对 Vue.js 的源码部分又有了进一步的了解,今天一起来学习 Vue 实例挂载是怎么一回事。
$mount
Vue 中我们是通过 $mount 实例方法去挂载 vm 的,$mount 方法在多个文件中都有定义。因为 $mount 这个方法的实现是和平台、构建方式都相关的,我们来分析一下带 compiler 版本的 $mount 实现。定位到 src/platform/web/entry-runtime-with-compiler.js。
// ...
const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && query(el)
// ...
}
而原型上的 $mount 方法其实是在 src/platform/web/runtime/index.js 就定义过的。
// public mount method
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && inBrowser ? query(el) : undefined
return mountComponent(this, el, hydrating)
}
之所以重新定义,是因为 runtime only 版本的其实是没有这块逻辑的,这块代码主要是为了复用,它可以被 runtime only 的 Vue 直接使用。继续分析这个函数做了哪些操作。
首先它对传入的 el 做了限制处理,可以是 string 或者 element , 然后调用了 query 方法,query 是在 src/platform/web/util/index.js 定义的。
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 如果是字符串,则调用 document.querySelector ,如果找不到的话,就报出一个警告,并且返回一个空的 div。否则就已经是 dom 对象了,所以直接返回就可以了。OK,回到主线上来,那么此时 el = el && query(el) 就已经是一个 dom 对象了。继续往下看,它又对 el 进行了一个判断,不让它挂载到 body 和 html 这样的根节点上,因为它是会覆盖的,如果挂上去,会把整个 html 文档都覆盖掉。
/* 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.`
)
return this
}
接下来的逻辑就比较关键了。它通过 this.$options 去判断有没有定义 render 方法。通过我们工程都是脚手架生成的,所以一般是是不会去手写 render,实际上它是可以手动定义的。
// ...
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}`,
this
)
}
}
} else if (template.nodeType) {
template = template.innerHTML
} else {
if (process.env.NODE_ENV !== 'production') {
warn('invalid template option:' + template, this)
}
return this
}
} else if (el) {
template = getOuterHTML(el)
}
// ...
顺着逻辑读下来,它会走到 template = getOuterHTML(el) ,看一下 getOuterHTML 是什么玩意。
/**
* Get outerHTML of elements, taking care
* of SVG elements in IE as well.
*/
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
}
}
在 MDN 中找到了关于 outerHTML 的定义
element DOM接口的outerHTML属性获取描述元素(包括其后代)的序列化HTML片段。它也可以设置为用从给定字符串解析的节点替换元素。
那此函数就简单了,判断有没有 el.outerHTML ,有则直接返回,没有则在外面包一层 div,然后执行 innerHTML ,其实就是 outerHTML API 的一种 polyfill 方法。这样的话就能在代码中拿到我们定义的最外层的 div ,注意,此时返回的是一个字符串。例如
<div id="app"></div>
下面的代码就是与编译相关的了,我们这次暂时不关注。 我们只需要知道
如果没有定义
render方法,则会把el或者template字符串转换成render方法。在 Vue 2.x 版本中,所有 Vue 的组件的渲染最终都需要render方法,无论我们是用单文件.vue方式开发组件,还是写了el或者template属性,最终都会转换成render方法,那么这个过程是 Vue 的一个“在线编译”的过程,它是调用compileToFunctions方法实现的。
有了 render 函数之后,会调用 mount 方法,这里的 mount 就是最开始定义的 ,它调用原型上的 $mount 做挂载。
const mount = Vue.prototype.$mount
紧接着又会执行到 src/platform/web/runtime/index.js 里,还是前面所说可供复用的方法,这里再贴一次代码。
// 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 方法支持传入 2 个参数,第一个是 el,它表示挂载的元素,可以是字符串,也可以是 DOM 对象,如果是字符串在浏览器环境下会调用 query 方法转换成 DOM 对象的。第二个参数是和服务端渲染相关,在浏览器环境下我们不需要传第二个参数。
$mount 方法实际上会去调用 mountComponent 方法,这个方法定义在 src/core/instance/lifecycle.js 文件中,我们来逐步分析一下。
首先通过 vm.$el = el 来做缓存,然后判断是否有 render 函数。
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) !== '#') ||
) {
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 函数,并且 template 也没有正确的转换成 render 函数,那么就执行 vm.$options.render = createEmptyVNode 去创建一个空 VNode 给它。接着判断如果定义了 template ,但是第一个不是 # ,或者是写了 render 函数,但是用的是 complier 版本吗,就会报一个警告啦,警告大致是说,使用了 runtime only 版本,然后又没写 render 函数,然后 template 是不可编译的。或者是两者都没有,就会报 else 里的另外一个警告。
接下来就是一堆关于 Vue 性能埋点的逻辑代码,暂时不看,它最后会走到这里。
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
vm._render() 可以理解为返回出来一个 VNode,而 hydrating 是服务端渲染相关,可以理解为就是 false。 那 updateComponent 是怎么执行的呢。它实际上调用了 new Watcher。
// 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 */)
Watcher 在这里起到两个作用,一个是初始化的时候会执行回调函数,另一个是当 vm 实例中的监测的数据发生变化的时候执行回调函数。
可以把理解为它是一个渲染 Wathcer ,看到它传了几个参数,vm 实例,updateComponent 函数,noop 空函数,配置对象,布尔值。找到 Wathcer 的定义位置,在 src/core/observe/watcher.js ,其实 Wathcer 也是一个 class ,它的构造函数是这样。
constructor (
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object,
isRenderWatcher?: boolean // 是否是渲染 watcher
)
this.vm = vm
if (isRenderWatcher) {
vm._watcher = this
}
vm._watchers.push(this)
然后在 vm 上添加了一个 _watcher ,并 push 到所有的 _watchers 里面。继续往下执行,可以找到 value = this.getter.call(vm, vm) ,调用 this.getter 也就是执行到 updateComponent 方法了,它会先调用 vm._render() 生成一个 VNode ,然后通过 vm._update() 挂载更新 DOM。
// 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
函数最后判断为根节点的时候设置
vm._isMounted为 true, 表示这个实例已经挂载了,同时执行mounted钩子函数。 这里注意vm.$vnode表示 Vue 实例的父虚拟 Node,所以它为 Null 则表示当前是根 Vue 的实例。
总结
这章主要分析了 $mount 的实现,和 mountComponent 方法的详细内容,下一章我们来分析 vm._update() 和 vm._render() 。