虚拟DOM是为性能而诞生的吗?

同样的,我们还是以问题的形式开启虚拟DOM的探索

Q1: “虚拟`DOM`是什么?” - what
  • A: “是JSDOM之间的一个映射缓存”
  • A: “在形态上表现为一个能够描述DOM结构及其属性信息的JS对象”

若对上述问题存在疑惑,可以先学习JSX的三个“大问题” 这一小节的内容

Q2: “`React`中的虚拟`DOM`大致是如何工作的?” - how
  • A: “在挂载阶段,React结合JSX的描述,构建出虚拟DOM树,通过ReactDOM.render实现虚拟DOM到真实DOM的映射”
  • A: “在更新阶段,页面变化先作用于虚拟DOM,虚拟DOMJS层借助算法对比出具体的改变,然后将这些改变作用于真实DOM

到这里,我们就粗糙的了解了虚拟DOMwhathow两大问题,接下来,让我们从一道经典的面试题,开启对why的探索

Q3: “为什么我们需要虚拟`DOM`?” - why
  • A: “DOM操作很慢,而JS却可以很快”
  • A: “直接操作DOM可能会导致频繁的回流和重绘,而JS不存在这些问题。因此虚拟DOM比原生DOM更快”

不过,答案真的是这样吗?让我们一起揪出虚拟DOM的定位及其主要解决的问题吧!

一、解决DOM操作的发展史

jQuery时期之前,前端页面更多的是“展示”,“交互”少之又少,开发者不需要太多操作DOM,原生JS足以。

1、jQuery时期

很快,人们开始追究丰富的用户体验,与之而来的就是大量的DOM操作,而原生JS提供的DOM API实在是太难用了,大大降低了开发效率。

jQuery的出世解决了API太难用这个问题。它将DOM API封装为相对简单和优雅的形式,并支持链式API调用,提供插件扩展能力,而且解决了跨浏览器的兼容工作,达到“写得更少,做得更多”的效果

2、虚拟DOM推广之前的“模版引擎”

jQueryDOM操作变得简单、快速,但它并不能从根本解决DOM操作量过大时前端侧的压力。即jQuery类比手持吸尘器,而模版引擎类比扫地机器人的雏形。它只需要接受命令,然后把下面的活干得漂漂亮亮的:

  • 读取HTML模板并解析它,分离出其中的JS信息
  • 将解析出的内容拼接成字符串,动态生成JS代码
  • 运行动态生成的JS代码,吐出“目标HTML
  • 将“目标HTML”赋值给innerHTML,触发渲染流水线,完成真实DOM的渲染

换句话说,当数据发生变化时,你不需要关心哪个DOM的数据变了,也不用手动去完成对应的DOM的修改,只需要关注数据和数据变化本身。

可惜的是,模版引擎出现的契机虽然是为了使用户界面和业务数据分离,但实际的应用场景基本局限在“实现高效的字符串拼接”。它更新DOM的方式是将已经渲染出的DOM整体注销后再整体重新渲染,没有更新缓冲,导致在DOM操作频繁的场景,可能会直接卡死。

3、数据驱动视图

模版引擎的数据驱动视图方案,核心问题在于对真实DOM的修改“一刀切”,导致DOM操作的范围过大、频率过高,进而可能导致糟糕的性能。那在修改的时候,新增一层假的DOM作为缓冲不就行了?

同样是将用户界面与数据分离,模版引擎的工作流是

将数据源塞进预置好的模板,吐出对应的`DOM`元素,挂载到界面

而在虚拟DOM的加持下,其工作流是

将数据源塞进预置好的“模板”,生成虚拟`DOM`,吐出对应的`DOM`元素,挂载到界面
  • 模板二字加了引号:虚拟DOM在实现上并不总是借助模板,比如React中使用的JSX,其本质不是模版,而是JS语法糖
  • 多出一层虚拟DOM作为缓冲层,其“差量更新”和“批量更新”可以确保虚拟DOM既能够提供高效的开发体验,又能保持过得去的性能

差量更新

DOM操作比较频繁时,它会先将前后两次的虚拟DOM树进行对比,定位出具体需要更新的部分,生成一个“补丁集”,最后只把“补丁”打在需要更新的那部分真实DOM

旧的虚拟DOM树   diff         patch
             ------> 补丁集 ------> 真实`DOM`
新的虚拟DOM树  

批量更新

在差量更新速度非常快的情况下,比如极短的时间内多次操作同一个DOM,用户实际上只能看到最后一次更新的效果。这种场景下,前面几次的更新动作虽然意义不大,但都会触发重新渲染流程,带来不必要高耗能操作。这时就需要批量更新,batch的作用是在缓冲每次生成的补丁集。

它会把收集到的多个“补丁集”暂存到队列中,再将最终的结果交给渲染函数,最终实现集中化的DOM批量更新。

二、虚拟DOM的性能

性能是个复杂度比较高的问题,需要结合具体的参照物、渲染的阶段、数据的吞吐量等各种要素进行分析讨论。

下面对比一下模板和虚拟DOM在性能开销的差异

模板渲染过程的工作流分为两步骤:

  • 1、动态生成HTML字符串,构建新的真实DOM - (JS行为)
  • 2、旧的DOM元素整体被新的DOM元素替换 - (DOM行为)

虚拟DOM渲染过程的工作流分为三步骤:

  • 1、构建新的虚拟DOM树 - (JS行为)
  • 2、通过算法对比出新旧两棵树的差异 - (JS行为)
  • 3、差异更新DOM - (DOM行为)

JS行为这个层面

  • 动态生成HTML字符串的过程本质是对字符串的拼接,对性能的消耗是有限的
  • 虚拟DOM的构建和算法比对,不可避免地涉及递归、遍历等耗时操作
  • 模版渲染胜出

DOM行为这个层面

DOM操作的能耗和JS计算的能耗不在一个量级,极少量的DOM操作耗费的性能足以支撑大量的JS计算

当整个发生了改变 - DOM更新的工作量基本一致

  • 虚拟DOM伴随着更大的JS计算,大概率不敌模版渲染

当只修改少量的数据 - DOM更新的工作量拉开差距

  • 虚拟DOM将在性能上具备绝对的优势

总结

看到这,我相信你也意识到,比对性能是以严格的限定条件为前提的。而在整个DOM操作的演化过程中,主要矛盾也并不在于性能,而在于研发体验和研发效率。而虚拟DOM的优越之处在于它能够提供更爽、更高效的研发模式,即函数式的UI编程方式,同时保持着还不错的性能。

虚拟DOM是对真实的渲染内容的一层抽象,是真实DOM的描述,因此,它可以实现“一次编码,多端运行”,即跨平台也是虚拟DOM的优越之处。