什么是虚拟DOM
虚拟DOM本质上是js和DOM之间的一个映射缓存,他在形态上表现为能够描述真实DOM及其属性信息的js对象,所以对于虚拟DOM来说我们知道把握住两点即可:
1、虚拟DOM是js对象
2、虚拟DOM是对真实DOM的描述
虚拟DOM是如何工作的
虚拟DOM在React组件的挂载阶段和更新阶段都非常的关键,其参与的工作流程如下:
- 挂载阶段
React将结合JSX的描述,构建出虚拟DOM树,然后通过ReactDOM.render实现虚拟DOM到真实DOM的映射。
- 更新阶段
页面的变化在作用于真实DOM之前,都要先作用于虚拟DOM,虚拟DOM将在js层借助算法对比出具体有哪些真实DOM需要被改变,然后再将这些改变作用于真实DOM。
为什要使用虚拟DOM
以前的回答
:
通过js去操纵我们的DOM很可能会大大降低我们的渲染效率,因为它会影响我们整个渲染流水线,比如通过调用appendChild()接口网body节点上添加元素,首先渲染引擎会帮我们把节点添加到body上,然后就会进行计算、布局、绘制、栅格化、合成等任务,这一过程也叫作重排,除了重排之外还有可能引起重绘和合成操作,另外对DOM的操作不当还有可能引发布局抖动的问题。
这样说确实有一定的道理,但我们再一细想一下,真的完全是这样吗?下面我们从DOM操作的演化过程来讲解。
原生js支配时期:
在初期,前端页面展示的属性远远强于其交互的属性,这就导致js的定位只是辅助,在很长一段时间里,前端工程师要花费大量的时间去实现静态的DOM,然后再去补充少量的js实现交互。在这个阶段我们不需要做太多的太复杂的DOM操作,所以原生js足矣。
JQuery时期:
到了JQuery时期,为了追求更加丰富的用户体验,随之而来的就是大量DOM操作需求带来的前端开发工作量激增,为了能够实现高效的开发,JQuery也就出现了,它将DOMcAPI封装为了相对简单和优雅的形式,同时解决了跨浏览器的兼容工作并且提供插件扩展机制。
早期模版引擎方案:
虽然JQuery帮助我们能够以更舒服的姿势操作DOM,但是它不能从根本上解决DOM操作量过大情况下前端的压力。
我们怎么来理解模版这个概念呢?我们来看一个例子:
const staff = [
{
name: '修言',
career: '前端'
},
{
name: '翠翠',
career: '编辑'
},
{
name: '花花',
career: '运营'
}
]
复制代码
现在我们想要在前端用表格展示这一堆数据,我们就可以遵循模版的语法,把它塞进模版(template)里去,下面就是一个典型的模版语法使用示例。
<table>
{% staff.forEach(function(person){ %}
<tr>
<td>{% student.name %}</td>
<td>{% student.age %}</td>
</tr>
{% }); %}
</table>
复制代码
这里我们用<% %>来包裹住逻辑代码,可以看出模版语法其实就是把JS和HTML结合在一起的一种规则,而模版引擎做的事情也非常容易理解,就是把staff这个数据读进去,塞到预置好的HTML模版里,把两者融合在一起,吐出一段目标字符串给你,这段字符串内容其实就是一份标准的、可用于渲染的HTML代码,它将对应一个DOM元素,最后将这个DOM元素挂载到页面中去,整个模版渲染流程就走完了。
这个过程也可以用伪代码来表示:
// 数据和模板融合出 HTML 代码
var targetDOM = template({data: students})
// 添加到页面中去
document.body.appendChild(targetDOM)
复制代码
所以我们总结下模版引擎做的几件事:
- 读取HTML模版并解析它,分离出其中的js信息
- 将解析出的内容拼接成字符串,动态生成js代码
- 运行动态生成的js代码,吐出
目标HTML
- 将
目标HTML
赋值给innerHTML,触发渲染流水线,完成真实DOM的渲染
使用模板引擎方案来渲染数据是非常爽的:每次数据发生变化时,我们都不用关心到底是哪里的数据变了,也不用手动去点对点完成 DOM 的修改。只需要关注的仅仅是数据和数据变化本身
,DOM 层面的改变模板引擎会帮我们做掉。
可惜的是,模板引擎出现的契机虽然是为了使用户界面与业务数据相分离,但实际的应用场景基本局限在“实现高效的字符串拼接”这一个点上,因此不能指望它去做太复杂的事情。尤其令人无法接受的是,它在性能上的表现并不尽如人意:由于不够“智能”,它更新DOM的方式是将已经渲染出 DOM整体注销后再整体重渲染
,并且不存在更新缓冲这一说。在DOM操作频繁的场景下,模板引擎可能会直接导致页面卡死
。
虚拟DOM:
先通过两张图来对比下模版引擎和虚拟DOM的工作方式,模版引擎如下:
而在虚拟DOM的支持下如下:
这里要注意这里模板
两个字,这是因为虚拟DOM在实现上并不总是借助模板,比如React就使用了JSX,JSX本质不是模板,而是一种使用体验和模板相似的JS语法糖。
区别就在于多出了一层虚拟DOM作为缓冲层。这个缓冲层带来的利好是:当DOM操作(渲染更新)比较频繁时,它会先将前后两次的虚拟DOM树进行对比,定位出具体需要更新的部分,生成一个“补丁集”,最后只把“补丁”打在需要更新的那部分真实DOM上,实现精准的“差量更新”。
React使用虚拟DOM是真的为了更好的性能吗?
在整个DOM操作的演化过程中,主要的矛盾并不在于性能,而是在于研发体验/研发效率
,虚拟DOM并不一定会带来更好的性能,React官方也从来没有把虚拟DOM作为性能层面上的卖点对外输出,虚拟DOM的优越之处在于它能够在提供更爽更高效的开发模式的同时,仍然保持一个还不错的性能
。
下面我们来对比下模板引擎和虚拟DOM:
从图中可以看出,模板渲染的步骤1,和虚拟 DOM 渲染的步骤1、2都属于 JS 范畴的行为,这两者是具备可比性的,我们放在一起来看:动态生成 HTML 字符串的过程本质是对字符串的拼接,对性能的消耗是有限的;而虚拟 DOM 的构建和 diff 过程逻辑则相对复杂,它不可避免地涉及递归、遍历等耗时操作。因此在 JS 行为这个层面,模板渲染胜出
。
模板渲染的步骤3,和虚拟 DOM 的步骤3 都属于 DOM 范畴的行为,两者具备可比性,因此我们仍然可以愉快地对比下去:模板渲染是全量更新,而虚拟 DOM 是差量更新。
乍一看好像差量更新一定比全量更新高效,但你需要考虑这样一种情况:数据内容变化非常大(或者说整个发生了改变),促使差量更新计算出来的结果和全量更新极为接近(或者说完全一样)。
当然,上面讨论的这种情况相对来说比较极端。在实际的开发中,更加高频的场景是这样的:我每次 setState 的时候只修改少量的数据,比如一个对象中的某几个属性,再比如一个数组中的某几个元素。在这样的场景下,模板渲染和虚拟 DOM 之间 DOM 操作量级的差距就完全拉开了,虚拟 DOM 将在性能上具备绝对的优势。
虚拟DOM到底解决了什么问题呢?
-
研发体验/研发效率的问题:
我们知道DOM操作的每次革新背后都是对前端开发效率和体验的进一步追求,虚拟DOM的出现为数据驱动视图这一思想提供了高度可用的载体,使得前端开发能够基于函数式UI的编程方式实现高效的声明式编程。
-
跨平台问题:
虚拟 DOM 是对真实渲染内容的一层抽象。若没有这一层抽象,那么视图层将和渲染平台紧密耦合在一起,为了描述同样的视图内容,你可能要分别在 Web 端和 Native 端写完全不同的两套甚至多套代码。但现在中间多了一层描述性的虚拟 DOM,它描述的东西可以是真实 DOM,也可以是iOS 界面、安卓界面、小程序……同一套虚拟 DOM,可以对接不同平台的渲染逻辑,从而实现“一次编码,多端运行”。
虚拟DOM的批量更新和双缓存
除了差量更新之外批量更新
也是虚拟DOM在性能方面所做的一个重要的努力,批量更新是通过batch
函数来处理的,在差量更新速度非常快的情况下(比如极短的时间多次操作同一个DOM),用户实际上只能看到最后一次更新的效果,在这种场景下,前面几次的更新动作虽然意义不大,但是都会触发重渲染流程,带来大量不必要的高耗能操作。
这时就需要请batch来帮忙了,batch的作用就是缓冲每次生成的补丁集
,它会把收集到的多个布丁集暂存到队列中,再将最终的结果交给渲染函数,最终实现集中化的DOM批量更新。
这种策略和我们的双缓存出不多,在处理图像的时候,屏幕要从前缓冲区读取数据然后显示,但是对于比较复杂的图形来说它可能需要经过几次计算才能得到最终结果,如果每计算完一部分就将结果写进缓冲区,就会造成页面效果是一部分一部分显示出来,在刷新页面的的过程中会让用户感到页面的闪烁。
而使用双缓存可以先让你将计算的中间结果缓存在另外一个缓冲区,等到全部的结果计算出来后,在将缓冲区的图形数据一次性复制到显示缓冲区,这使得整个图像的输出非常稳定。而虚拟DOM也是的,他会在完成一次完整的操作之后,再把结果应用到DOM上,这样就能减少一些不必要的更新。