在《new Vue 发生了什么》一文中,详细地讲解其内部究竟是怎样实现的。从该文可知,如果在实例化 Vue 时传入的参数 el 不为空时,那么在其内部会自动调用 Vue 实例上函数 $mount 进行挂载;否则会进行手动挂载。那么,Vue 源码中是如何实现挂载的呢?这将是本文要探究的内容。
mount 挂载实现原理
沿着主线将其实现逻辑整理成一张图,如下:
接下来根据这张图一步一步地讲解其内部是如何实现的?
在函数 mount 实现逻辑中,主要可以分为 4 步,分别如下:
1、通过 el 查询元素
el = el && query(el)
/**
* Query an element selector if it's not an element already.
*/
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
}
}
通过 typeof 判断 el 类型,如果是 string 类型,则调用 document.querySelector(el) 查询元素,并将其返回;否则直接返回。
2、检查 Vue 实例是否挂载在 html 或者 body
/* 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
}
如果 Vue 实例挂载在 html 或者 body 上,则抛出告警,并且终止程序。
3、根据是否有 render 函数来决定是否需要将 template 编译成 render 函数
如果传参没有包含 render 函数,则使用 template 编写,需要将 template 编译成 render 函数。
4、调用保存的函数 mount
准备工作做好了,此时调用在重新定义 mount 前保存的 mount,即在文件 src/platforms/web/runtime/index.js 定义的函数 mount。在其内部实现中,核心代码就一行:
// public mount method
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && inBrowser ? query(el) : undefined
return mountComponent(this, el, hydrating)
}
即调用函数 mountComponent(this, el, hydrating)。
mountComponent 内部实现
1、检查 Vue 实例是否有 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) !== '#') ||
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
)
}
}
}
如果 Vue 实例没有 render 函数,则会其赋值默认函数:createEmptyVNode,作用创建空的虚拟结点。与此同时,在开发环境下会抛出告警。
2、调用生命周期函数 beforeMount
callHook(vm, 'beforeMount')
3、定义函数 updateComponent
/* istanbul ignore if */
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 = () => {
vm._update(vm._render(), hydrating)
}
}
在函数 updateComponent 内部实现中,有两个比较重要的方法:vm._render 和 vm._update。vm._render 作用是将 Vue 实例渲染成一个虚拟 Node;而 vm._update 作用是将虚拟 Node 渲染成真实 DOM。后续章节会详细分析这两个方法的内部实现逻辑。
4、实例化 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 时,需要传入 5 个参数,其中有一个是刚刚定义的函数:updateComponent,作为参数传入,此后在其回调函数中会被执行;除此之外,isRenderWatcher 值为 true,表示此 Watcher 是渲染 Watcher,作用是为了区别其他 Watcher,比如计算属性 Watcher。那么来看下Watcher 内部是如何实现的?
Watcher 在这里有两个作用:一个是在初始化时会执行其回调函数,也就是作为参数传入的函数 updateComponent;另一个是当 Vue 实例监测到数据发生变化时,也会执行其回调函数,即响应式原理。
那么这里主要分析 Watcher 初始化部分,即其构造函数是如何实现的?构造函数接收 5 个参数:
export default class Watcher {
...
constructor (
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object,
isRenderWatcher?: boolean
) {
...
}
}
vm:Vue 实例expOrFn:数据类型为字符串或者函数cb:回调函数options:可选参数,数据类型为ObjectisRenderWatcher:表示是否为渲染Watcher
isRenderWatcher 参数体现在这行代码:
if (isRenderWatcher) {
vm._watcher = this
}
对于包含子组件的组件,子组件的挂载需要依赖已经定义 vm._watcher 。重点来看下如何解析参数 expOrFn,具体实现如下:
// parse expression for getter
if (typeof expOrFn === 'function') {
this.getter = expOrFn
} else {
this.getter = parsePath(expOrFn)
if (!this.getter) {
this.getter = noop
process.env.NODE_ENV !== 'production' && warn(
`Failed watching path: "${expOrFn}" ` +
'Watcher only accepts simple dot-delimited paths. ' +
'For full control, use a function instead.',
vm
)
}
}
如果 expOrFn 传入的是一个函数,则将其赋值给 getter;否则是一个字符串,应该是一个路径,调用函数 parsePath 对其进行解析,为空的话则抛出告警。
最终执行回调函数在最后一行代码,即
this.value = this.lazy ? undefined : this.get()
由于 lazy 值为 false,则会调用 Watcher 实例方法 get。其实现逻辑如下:
/**
* Evaluate the getter, and re-collect dependencies.
*/
get () {
pushTarget(this)
let value
const vm = this.vm
try {
value = this.getter.call(vm, vm)
} catch (e) {
if (this.user) {
handleError(e, vm, `getter for watcher "${this.expression}"`)
} else {
throw e
}
} finally {
// "touch" every property so they are all tracked as
// dependencies for deep watching
if (this.deep) {
traverse(value)
}
popTarget()
this.cleanupDeps()
}
return value
}
核心代码就一行:value = this.getter.call(vm, vm)。即调用作为参数传进来的函数:updateComponent,将 Vue 实例渲染成虚拟 Node,再将虚拟 Node 渲染成真实 DOM。
5、完成 Vue 实例挂载
// 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')
}
此时判断为根节点时设置 vm._isMounted 为 true,表示 Vue 实例已经完成挂载;同时执行生命周期函数 mounted。需要注意的是
vm.$vnode 表示 Vue 实例的父虚拟 Node。
至此,Vue 源码如何实现挂载分析完了,其中涉及到 template 如何编译成 render 函数、vm._render 将 Vue 实例渲染成虚拟 DOM、vm._update 将虚拟 DOM 渲染成真实 DOM 的实现逻辑后续章节会详情介绍。