继上一节内容,我们将
Vue
复杂的挂载流程通过图解流程,代码分析的方式简单梳理了一遍,最后也讲到了模板编译的大致流程。然而在挂载的核心处,我们并没有分析模板编译后渲染函数是如何转换为可视化DOM
节点的。因此这一章节,我们将重新回到Vue
实例挂载的最后一个环节:渲染DOM
节点。在渲染真实DOM
的过程中,Vue
引进了虚拟DOM
的概念,这是Vue
架构设计中另一个重要的理念。虚拟DOM
作为JS
对象和真实DOM
中间的一个缓冲层,对JS
频繁操作DOM
的引起的性能问题有很好的缓解作用。
4.1 Virtual DOM
4.1.1 浏览器的渲染流程
当浏览器接收到一个Html
文件时,JS
引擎和浏览器的渲染引擎便开始工作了。从渲染引擎的角度,它首先会将html
文件解析成一个DOM
树,与此同时,浏览器将识别并加载CSS
样式,并和DOM
树一起合并为一个渲染树。有了渲染树后,渲染引擎将计算所有元素的位置信息,最后通过绘制,在屏幕上打印最终的内容。JS
引擎和渲染引擎虽然是两个独立的线程,但是JS引擎却可以触发渲染引擎工作,当我们通过脚本去修改元素位置或外观时,JS
引擎会利用DOM
相关的API
方法去操作DOM
对象,此时渲染引擎变开始工作,渲染引擎会触发回流或者重绘。下面是回流重绘的两个概念:
- 回流: 当我们对
DOM
的修改引发了元素尺寸的变化时,浏览器需要重新计算元素的大小和位置,最后将重新计算的结果绘制出来,这个过程称为回流。 - 重绘: 当我们对
DOM
的修改只单纯改变元素的颜色时,浏览器此时并不需要重新计算元素的大小和位置,而只要重新绘制新样式。这个过程称为重绘。
很显然回流比重绘更加耗费性能。
通过了解浏览器基本的渲染机制,我们很容易联想到当不断的通过JS
修改DOM
时,不经意间会触发到渲染引擎的回流或者重绘,这个性能开销是非常巨大的。因此为了降低开销,我们需要做的是尽可能减少DOM
操作。有什么方法可以做到呢?
4.1.2 缓冲层-虚拟DOM
虚拟DOM
是为了解决频繁操作DOM
引发性能问题的产物。虚拟DOM
(下面称为Virtual DOM
)是将页面的状态抽象为JS
对象的形式,本质上是JS
和真实DOM
的中间层,当我们想用JS
脚本大批量进行DOM
操作时,会优先作用于Virtual DOM
这个JS
对象,最后通过对比将要改动的部分通知并更新到真实的DOM
。尽管最终还是操作真实的DOM
,但Virtual DOM
可以将多个改动合并成一个批量的操作,从而减少 DOM
重排的次数,进而缩短了生成渲染树和绘制所花的时间。
我们看一个真实的DOM
包含了什么:
浏览器将一个真实DOM
设计得很复杂,不仅包含了自身的属性描述,大小位置等定义,也囊括了DOM
拥有的浏览器事件等。正因为如此复杂的结构,我们频繁去操作DOM
或多或少会带来浏览器的性能问题。而作为数据和真实DOM
之间的一层缓冲,Virtual DOM
只是用来映射到真实DOM
的渲染,因此不需要包含操作 DOM
的方法,它只要在对象中重点关注几个属性即可。
// 真实DOM
<div id="real"><span>dom</span></div>
// 真实DOM对应的JS对象
{
tag: 'div',
data: {
id: 'real'
},
children: [{
tag: 'span',
children: 'dom'
}]
}
4.2 Vnode
Vue
在渲染机制的优化上,同样引进了virtual dom
的概念,它是用Vnode
这个构造函数去描述一个DOM
节点。
4.2.1 Vnode构造函数
var VNode = function VNode (tag,data,children,text,elm,context,componentOptions,asyncFactory) {
this.tag = tag; // 标签
this.data = data; // 数据
this.children = children; // 子节点
this.text = text;
···
···
};
Vnode
定义的属性差不多有20几个,显然用Vnode
对象要比真实DOM
对象描述的内容要简单得多,它只用来单纯描述节点的关键属性,例如标签名,数据,子节点等。并没有保留跟浏览器相关的DOM
方法。除此之外,Vnode
也会有其他的属性用来扩展Vue
的灵活性。
源码中也定义了创建Vnode
的相关方法。参考Vue3源码视频讲解:进入学习
4.2.2 创建Vnode注释节点
// 创建注释vnode节点
var createEmptyVNode = function (text) {
if ( text === void 0 ) text = '';
var node = new VNode();
node.text = text;
node.isComment = true; // 标记注释节点
return node
};
4.2.3 创建Vnode文本节点
// 创建文本vnode节点
function createTextVNode (val) {
return new VNode(undefined, undefined, undefined, String(val))
}
4.2.4 克隆vnode
function cloneVNode (vnode) {
var cloned = new VNode(
vnode.tag,
vnode.data,
vnode.children && vnode.children.slice(),
vnode.text,
vnode.elm,
vnode.context,
vnode.componentOptions,
vnode.asyncFactory
);
cloned.ns = vnode.ns;
cloned.isStatic = vnode.isStatic;
cloned.key = vnode.key;
cloned.isComment = vnode.isComment;
cloned.fnContext = vnode.fnContext;
cloned.fnOptions = vnode.fnOptions;
cloned.fnScopeId = vnode.fnScopeId;
cloned.asyncMeta = vnode.asyncMeta;
cloned.isCloned = true;
return cloned
}
注意:cloneVnode
对Vnode
的克隆只是一层浅拷贝,它不会对子节点进行深度克隆。