1. 什么是虚拟dom
虚拟dom是React的基础,也是核心概念之一。在上一张中我们介绍了虚拟dom一方面通过和diff算法的结合极大的提升了页面交互的性能,同时它也是跨平台编码的基础,包括了服务端渲染(node)和native编码(React Native)。
虚拟dom其实相当于在js和真实dom之间建立了一个缓存,当我们需要操作dom时,会先通过diff算法计算出虚拟dom变化的最小差异,并只针对最小差异更新真实dom
回到什么是虚拟dom,虚拟dom本质上是一个js对象。举个例子:
<ul id='list'>
<li class='item'>1</li>
<li class='item'>2</li>
<li class='item'>3</li>
</ul>
上面的html转化成虚拟dom如下所示:
const tree = {
tagName: 'ul',
props: {
id: 'list'
},
children: [
{tagName: 'li', props: {class: 'item'}, children: ['1']},
{tagName: 'li', props: {class: 'item'}, children: ['2']},
{tagName: 'li', props: {class: 'item'}, children: ['3']},
]
}
2. 为什么需要虚拟dom
虚拟dom带来的好处主要有3点:
1. 提升性能:众所周知操作真实dom慢,虚拟dom快。其具体原因在于当我们操作真实dom的时候会导致浏览器的重绘(repaint)和回流(reflow),当我们操作真实dom时其实浏览器做了以下操作:
1. 构建dom树
2. 构建css样式表树
3. 将dom树和css样式树结合起来,形成待渲染的render树
4. 确定render树上每个节点的位置、尺寸等信息(reflow)。
5. 将内容渲染到对应的位置(paint)。
2. 一次开发,多端渲染。在node环境中没有dom,但借助于虚拟dom,我们可以模拟真实dom并进行服务端渲染,同时在跨端开发领域,React Native、 Weex等框架也都是基于虚拟dom来实现的。
3. 结合Jsx语法,虚拟dom提供了一种新的开发方式,可以声明式的开发页面,同时使组件化的开发方式得以广泛应用,提升了团队开发的代码的可读性和可维护性。
3. diff算法
提到虚拟dom就不得不提到diff算法,diff算法故名思议就是用来计算两个树的差异的算法,应用在React中用来计算两个虚拟dom树的最小差异,从而更新。然而diff算法的时间复杂度是O(n^3),这在react中显然是不能接受的,所以React对传统的diff算法进行了一些改造。
1. tree diff:只比较同层级的节点。对于整个树的diff,由于用户跨层级操作dom的情况少到基本可以忽略不计。这个优化可以极大的提升diff算法的性能,时间复杂度从O(n^3)降到了O(n)。
2. component diff:对于组件的diff操作核心是两点,第一判断两个组件是不是同一类型的组件,如果不是则替换掉该组件及其子组件,如果是同一类型组件则继续进行tree diff。注意,在这里React提供了一个componentShouldMount的声明周期给到开发者,当两次渲染的组件没有任何变化时可以在该生命周期中返回false,这样则直接渲染原组件内容,而不用在进行递归的diff比较,可以极大的节省性能。
3. element diff:当进行element比较时,React主要有添加、删除和移动三种操作。需要注意的是列表组件的比较,举个例子:
循环比较新老节点,当发现新节点和老节点不一样时,会删除老节点,添加新节点。当我们做上述更改时,做了4次删除和4次新增操作。React发现这是非常低效的,其实元素没有任何变化,只是位置发生了改变。针对这个React提出在渲染列表时给每个节点添加唯一的key值。并将老节点的key值抽到一个Map中,遍历新节点时,判断该节点是否在老节点中存在,如果存在则只进行移动操作。这个优化同样能极大的改善diff的性能。
总结
- 虚拟dom是对真实dom的抽象表达,本质是一个普通object对象,相当于在js和真实dom之间搭起的一层缓存。
- 虚拟dom的优势在于提升页面渲染性能,一次开发多端渲染。
- diff算法搭配虚拟dom可以极大提升页面性能。
- 在传统diff算法的基础上React做了几点优化:1. 只比较同层级的节点。 2. 提供componentShouldUpdate钩子函数 3. 添加key属性提高元素渲染性能。