主要梳理了一下$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
钩子函数