前面几节我们从
new Vue
创建实例开始,介绍了创建实例时执行初始化流程中的重要两步,配置选项的资源合并,以及响应式系统的核心思想,数据代理。在合并章节,我们对Vue
丰富的选项合并策略有了基本的认知,在数据代理章节我们又对代理拦截的意义和使用场景有了深入的认识。按照Vue
源码的设计思路,初始化过程还会进行很多操作,例如组件之间创建关联,初始化事件中心,初始化数据并建立响应式系统等,并最终将模板和数据渲染成为dom
节点。如果直接按流程的先后顺序分析每个步骤的实现细节,会有很多概念很难理解。因此在这一章节,我们先重点分析一个概念,实例的挂载渲染流程。
3.1 Runtime Only VS Runtime + Compiler
在正文开始之前,我们先了解一下vue
基于源码构建的两个版本,一个是runtime only
(一个只包含运行时的版本),另一个是runtime + compiler
(一个同时包含编译器和运行时的版本)。而两个版本的区别仅在于后者包含了一个编译器。
什么是编译器,百度百科这样解释道:
简单讲,编译器就是将“一种语言(通常为高级语言)”翻译为“另一种语言(通常为低级语言)”的程序。一个现代编译器的主要工作流程:源代码 (source code) → 预处理器 (preprocessor) → 编译器 (compiler) → 目标代码 (object code) → 链接器 (Linker) → 可执行程序 (executables)。
通俗点讲,编译器是一个提供了将源代码转化为目标代码的工具。从Vue
的角度出发,内置的编译器实现了将template
模板转换编译为可执行javascript
脚本的功能。
3.1.1 Runtime + Compiler
一个完整的Vue
版本是包含编译器的,我们可以使用template
进行模板编写。编译器会自动将模板字符串编译成渲染函数的代码,源码中就是render
函数。
如果你需要在客户端编译模板 (比如传入一个字符串给 template
选项,或挂载到一个元素上并以其 DOM
内部的 HTML 作为模板),就需要一个包含编译器的版本。
// 需要编译器的版本
new Vue({
template: '<div>{
{ hi }}</div>'
})
3.1.2 Runtime Only
只包含运行时的代码拥有创建Vue
实例、渲染并处理Virtual DOM
等功能,基本上就是除去编译器外的完整代码。Runtime Only
的适用场景有两种:
1.我们在选项中通过手写render
函数去定义渲染过程,这个时候并不需要包含编译器的版本便可完整执行。
// 不需要编译器
new Vue({
render (h) {
return h('div', this.hi)
}
})
2.借助vue-loader
这样的编译工具进行编译,当我们利用webpack
进行Vue
的工程化开发时,常常会利用vue-loader
对.vue
进行编译,尽管我们也是利用template
模板标签去书写代码,但是此时的Vue
已经不需要利用编译器去负责模板的编译工作了,这个过程交给了插件去实现。
很明显,编译过程对性能会造成一定的损耗,并且由于加入了编译的流程代码,Vue
代码的总体积也更加庞大(运行时版本相比完整版体积要小大约 30%)。因此在实际开发中,我们需要借助像webpack
的vue-loader
这类工具进行编译,将Vue
对模板的编译阶段合并到webpack
的构建流程中,这样不仅减少了生产环境代码的体积,也大大提高了运行时的性能,一举两得。参考Vue3源码视频讲解:进入学习
3.2 实例挂载的基本思路
有了上面的基础,我们回头看初始化_init
的代码,在代码中我们观察到initProxy
后有一系列的函数调用,这些函数包括了创建组件关联,初始化事件处理,定义渲染函数,构建数据响应式系统等,最后还有一段代码,在el
存在的情况下,实例会调用$mount
进行实例挂载。
Vue.prototype._init = function (options) {
···
// 选项合并
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {
},
vm
);
// 数据代理
initProxy(vm);
vm._self = vm;
initLifecycle(vm);
// 初始化事件处理
initEvents(vm);
// 定义渲染函数
initRender(vm);
// 构建响应式系统
initState(vm);
// 等等
···
if (vm.$options.el) {
vm.$mount(vm.$options.el);
}
}
以手写template
模板为例,理清楚什么是挂载。我们会在选项中传递template
为属性的模板字符串,如<div>{
{message}}</div>
,最终这个模板字符串通过中间过程将其转成真实的DOM
节点,并挂载到选项中el
代表的根节点上完成视图渲染。这个中间过程就是接下来要分析的挂载流程。
Vue
挂载的流程是比较复杂的,接下来我将通过流程图,代码分析两种方式为大家展示挂载的真实过程。
3.2.1 流程图
如果用一句话概括挂载的过程,可以描述为确认挂载节点,编译模板为render
函数,渲染函数转换Virtual DOM
,创建真实节点。
3.2.2 代码分析
接下来我们从代码的角度去剖析挂载的流程。挂载的代码较多,下面只提取骨架相关的部分代码。
// 内部真正实现挂载的方法
Vue.prototype.$mount = function (el, hydrating) {
el = el && inBrowser ? query(el) : undefined;
// 调用mountComponent方法挂载
return mountComponent(this, el, hydrating)
};
// 缓存了原型上的 $mount 方法
var mount = Vue.prototype.$mount;
// 重新定义$mount,为包含编译器和不包含编译器的版本提供不同封装,最终调用的是缓存原型上的$mount方法
Vue.prototype.$mount = function (el, hydrating) {
// 获取挂载元素
el = el && query(el);
// 挂载元素不能为跟节点
if (el === document.body || el === document.documentElement) {
warn(
"Do not mount Vue to <html> or <body> - mount to normal elements instead."