在_init
的最后一步中是调用了$mount
$mount
在src/platform/web/entry-runtime-with-compiler.js
、src/platform/weex/runtime/index.js
都有定义。因为$mount
方法的实现是和平台、构建方式都相关的。
完整版(Runtime + Compiler
)的Vue
中,$mount
函数在src/platform/web/entry-runtime-with-compiler.js
中被重写:
// src/platforms/web/entry-runtime-with-compiler.js
// 备注: 这里先拿到了`Runtime Only`版本的`$mount`方法,然后进行重写
const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
// 备注: 如果el是一个字符串,就调用querySelector获取节点并返回,如果节点不存在就抛出警告并创建一个div节点
// 如果是一个节点就直接返回
el = el && query(el)
// ...
// 最后又调用了`Runtime Only`版本的`$mount`方法
return mount.call(this, el, hydrating);
}
参数的类型检查表明el
可以是字符串或DOM
节点。
// 这里去检查el是不是根节点(html)、(body),如果是就抛出警告并停止挂载。
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
// 备注: 判断render函数是否存在,不存在就要处理一下
if (!options.render) {
let template = options.template
// 备注: 判断有无template
if (template) {
// 备注:如果有template的处理
// 备注: 如果是字符串而且是id选择器,通过idToTemplate方法拿到相应节点,如果拿不到会抛出警告。
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
)
}
}
// 备注: 如果template是一个节点,那么获取它的innerHTML
} else if (template.nodeType) {
template = template.innerHTML
// 备注: 如果template既不是字符串也不是一个节点,那么抛出警告并结束挂载。
} else {
if (process.env.NODE_ENV !== 'production') {
warn('invalid template option:' + template, this)
}
return this
}
} else if (el) {
// 备注: 如果template不存在,接着判断el是否存在,存在则执行template = getOuterHTML(el)
// getOuterHTML中先判断el.outerHTML是否存在,有就返回outerHTML
// 如果没有(ie9-11中 svg标签元素是没有innerHTML 和 outerHTML 这两个属性的
// 对以上情况的兼容处理: 在el的外面包装了一层div,然后获取该div的innerHTML
template = getOuterHTML(el)
}
// 备注: 到这里无论是template还是el都被转为了字符串模板
// ================================================
// 备注: template可能是空字符串,要做一下判断
if (template) {
// 性能追踪开始
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
mark('compile')
}
// 调用compileToFunctions返回render函数,并挂载到options.render上
// 在 Vue 2.0 版本中,所有 Vue 的组件的渲染最终都需要 render 方法,
// 无论我们是用单文件 .vue 方式开发组件,还是写了 el 或者 template 属性,最终都会转换成 render 方法,
// 这个过程是 Vue 的一个“在线编译”的过程
const { render, staticRenderFns } = compileToFunctions(template, {
outputSourceRange: process.env.NODE_ENV !== 'production',
shouldDecodeNewlines,
shouldDecodeNewlinesForHref,
delimiters: options.delimiters,
comments: options.comments
}, this)
options.render = render
options.staticRenderFns = staticRenderFns
// 性能追踪结束
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
mark('compile end')
measure(`vue ${this._name} compile`, 'compile', 'compile end')
}
}
// 调用之前缓存的在src/platforms/web/runtime/index.js中定义的$mount函数:
return mount.call(this, el, hydrating)
}
// src/platforms/web/runtime/index.js
// 这里的 $mount 又把 el 从字符串转换成了节点然后传给了 mountComponent 函数
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && inBrowser ? query(el) : undefined
return mountComponent(this, el, hydrating)
}
// src/core/instance/lifecycle.js
export function mountComponent (
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
// 先将el保存到vm.$el上
vm.$el = el
// 然后判断前面的template是否被正确的转换成了render函数
if (!vm.$options.render) {
// 如果转换失败,将createEmptyVNode作为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函数是生命周期相关
callHook(vm, 'beforeMount')
let updateComponent
// 性能追踪
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 = () => {
// _render: 调用 vm.$options.render 函数并返回生成的虚拟节点(VNode)
// _update: 将 VNode 渲染成真实DOM
vm._update(vm._render(), hydrating)
}
}
// 这里创建了一个 Watcher 实例,显然是与响应式数据相关的
vm._watcher = new Watcher(vm, updateComponent, noop)
hydrating = false
// 函数最后判断为根节点的时候设置 vm._isMounted 为 true, 表示这个实例已经挂载了,
// 同时执行 mounted 钩子函数。
if (vm.$vnode == null) {
vm._isMounted = true
callHook(vm, 'mounted')
}
return vm
}
总结
在上一节中,我们通过_init
函数,合并了配置项、初始化生命周期、初始化事件中心、初始化渲染、初始化 data、props、computed、watcher 之后,调用了$mount
这个函数。
$mount
这个函数在多个文件中都有定义,本次分析主要从src/platforms/web/entry-runtime-with-compiler.js
这个文件入手,它先是保存了原先在vue上的$mount
函数,然后进行改造:
- 是否能找到这个节点;
- 判断是否是
html
或body
节点; - 判断有
render
无函数,如果没有,判断有无template,总之,这一步无论是template
还是el
都被转为了字符串模板,之后调用调用compileToFunctions
返回render函数,并挂载到options.render
上; - 判断字符串目标是否是空的,如果不是就调用原先在vue上的
$mount
函数; - 原先在vue上的
$mount
函数又把el
从字符串转换成了节点然后传给了mountComponent
函数; mountComponent
会判断有无render
函数,有的话就调用生命周期函数beforeMount
,- 调用
updateComponent
; - 创建
Watch
(初始化的时候会执行回调函数;另一个是当vm
实例中监测的数据发生变化的时候执行回调函数); - 最后判断为根节点的时候设置
vm._isMounted
为true
, 表示这个实例已经挂载了,同时执行mounted
钩子函数。
接下来我们要学习一下_render
函数和_update函数
:
updateComponent = () => {
// _render: 调用 vm.$options.render 函数并返回生成的虚拟节点(VNode)
// _update: 将 VNode 渲染成真实DOM
vm._update(vm._render(), hydrating)
}