主要梳理了一下$mount的执行和构建过程。文章重点有两块:1、公共的 $mount 方法的创建,2、带编译模板的 vue 给 $mount 扩展了编译部分。
上次梳理了vue入口文件的怎么查找,这次看一看$mount。由于后面尚未学习,只简单梳理流程。
1、扩展$mount
首先打开entry-runtime-with-compiler.js文件,这里我们可以看到对$mount的扩展:

先用 mount 变量存储Vue.prototype.$mount备用,接着重新给Vue.prototype.$mount赋值。注意这里是给$mount添加编译模块,所以不带编译器的vue文件是不会运行这块代码的。
我们可以看到,函数接受两个参数,第一个参数是一个字符串或者一个Dom元素,第二个参数暂时不管。函数内部首先尝试获取Dom元素,其中query函数如下:
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
}
}
可以看到其实就是做了下判断。如果是字符串则调用querySelector获取Dom,否则直接返回。
接着判断得到的Dom是不是body或者html元素,如果是则抛出警告。并return当前实例。

然后就是最重要的部分了,我们在new Vue(options)的时候通常会传el或者template,在vue项目中常见的是利用了render函数。它们的顺序是怎样呢?看源码就可以发现,render > template > el。把if判断折叠起来看起来很清晰:

如果传入了render,则直接调用mount.call,否则判断是否有template,尝试把template转化为render函数。如果没有template则判断是否有el,有的话则调用getOuterHTML得到el的outerHTML,getOuterHTML代码如下:
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
}
}
通过这个函数得到一串字符串,例如,下面这个例子:
<div id="app">
<span>{{ msg }}</span>
</div>
new Vue({
el: "#app",
data: {
msg: "hello, world!"
},
mounted() {
console.log(this.$el.outerHTML); // <div id="app"><span>hello, world!</span></div>
console.log(this.$el.innerHTML); // <span>hello, world!</span>
}
});
因此我们也可以知道getOuterHTML是将el转化成template,else部分是对el.outerHTML的模拟。因此核心部分是两个if (template)的内容。
先看第一个:
if (template) {
if (typeof template === 'string') {
if (template.charAt(0) === '#') {
template = idToTemplate(template) // 得到 template 的 innerHTML
/* 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
}
}
这块做的事情很简单,就是拿到template的innerHTML,调用了idToTemplate来将template传入id时转成template字符串:
const idToTemplate = cached(id => {
const el = query(id)
return el && el.innerHTML
})
再看第二个if:
if (template) {
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
mark('compile')
}
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
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
mark('compile end')
measure(`vue ${this._name} compile`, 'compile', 'compile end')
}
}
这块的主要部分就是通过compileToFunctions将template转成两个函数:render、staticRenderFns。具体编译部分暂时跳过。
因此总结下就是扩展的这段代码就是为了得到render函数,而不带编译器的vue由于有vue-loader提前执行了编译,所以不需要执行上述代码。
2、公共 $mount 方法
从上述代码可以看到最终扩展的代码也还是执行了之前保存的 mount 方法。我们来看看mount方法的真面目。
从entry-runtime-with-compiler.js可以看到Vue的导入来源:

打开文件找到如下代码:

由于这块是运行时执行的,因此关于前文对el的判断这里又做了一次。最后调用了mountComponent,可见重点是这个函数,在core/instance/lifecycle.js找到它:

首先将el保存在vm.$el,接着判断是否存在render,不存在则尝试获取。if判断是用来查看是否在不带编译器的vue中传入了el或者template,抛出一个警告。接着调用beforeMount这个生命周期钩子,并根据不同环境生成不同的updateComponent用来更新组件。

再往下看,我们发现它调用了Watcher,这是因为更新组件不仅发生在初始渲染,还发生在数据响应式变化后。此时的Watcher被称作渲染Watcher(render watcher)。

最后调用mounted这个生命周期钩子,并将vm实例返回。因此主要逻辑在Watcher中:

对比Watcher的实例化,我们可以看到传入构造函数的分别是:vm实例、updateComponent、noop(一个空函数)、一个对象,内部有一个before方法调用beforeUpdate钩子、true(表明这是个渲染watcher)。首先保存实例,然后在实例上挂载一个_watcher属性保存当前watcher实例。跳过一段初始化代码,直接看主要部分:

这段代码主要是给getter赋值为updateComponent,用于后续更新组件。然后调用this.get()触发更新:
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
}
这段就是为了调用updateComponent,而updateComponent内部调用了_update进行更新:

以上就是个人总结的$mount的执行流程,由于许多部分尚未学习,跳过了很多,总结下就是:
- 判断
el,调用mountComponent - 获取
render函数,调用beforeMount钩子函数 - 初始化
updateComponent - 实例化一个渲染
Watcher,在里面监听变化,调用updateComponent更新视图 - 调用
mounted钩子函数
本文详细梳理了Vue中$mount的执行和构建过程,包括$mount方法的扩展,如何处理el、template和render,以及编译模板的过程。重点关注了编译template到render函数的步骤,同时介绍了$mount内部如何调用mountComponent、Watcher等关键组件,完成组件的挂载和视图更新。
1453

被折叠的 条评论
为什么被折叠?



