什么是虚拟 DOM
虚拟 DOM(Virtual DOM)本质上是 JS 和 DOM 之间的一个映射缓存,它在形态上表现为一个能够描述 DOM 结构及其属性信息的 JS 对象
虚拟 DOM 在 React 组件的挂载阶段和更新阶段都会作为"关键人物"出镜,其参与的工作流程如下:
-
挂载阶段,React 将结合 JSX 的描述,构建出虚拟 DOM 树,然后通过 ReactDOM.render 实现虚拟 DOM 到真实 DOM 的映射(触发渲染流水线);
-
更新阶段,页面的变化在作用于真实 DOM 之前,会先作用于虚拟 DOM,虚拟 DOM 将在 JS 层借助算法先对比出具体有哪些真实 DOM 需要被改变,然后再将这些改变作用于真实 DOM
虚拟 DOM 的由来
React 的原型是 XHP,该框架于 2010 年开源。Facebook 创建 XHP 的目的主要有两点。
-
简化前端开发,按照现在流行的说法叫后端赋能,让后端开发人员能够快速交付页面。
-
避免跨站点脚本攻击,也就是常说的 XSS, Facebook 拥有庞大的站点,很容易因为一处暴露 XSS 而造成整体风险。XSS 不会直接攻击网页,而是通过嵌入 JavaScript 代码的方式,将恶意攻击附加到用户的请求中来攻击用户。它可以被用作窃取用户信息,或者恶意增删用户的一些资料。而 XHP 的优势就在于可以默认启用 XSS 保护。
Facebook 一开始的初衷就是简化前端开发、防止 XSS。它的解决方案也很粗暴,即不要直接操作 DOM,而是通过虚拟 DOM 规避风险。因为直接操作 DOM 可能会带来 XSS 的风险,也可能因为技术水平的限制,带来性能问题。如果你心爱的东西不喜欢有人去触碰,最好的方法就是把它封起来,与使用者相隔离,因此有了今天所看到的虚拟 DOM
各种 DOM 解决方案
原生 JS 支配下的"人肉 DOM"时期
在前端这个工种的萌芽阶段,前端页面"展示"的属性远远强于其"交互"的属性,这就导致 JS 的定位只能是"辅助":在很长一段时间里,前端工程师们会花费大量的时间去实现静态的 DOM,待一切结束后,再补充少量 JS,实现一些类似于拖拽、隐藏、幻灯片之类的"特效"。
在这个阶段,作为前端开发者来说,虽然我们一无所有,但过得很快乐------简单的业务需求决定了我们不需要去做太多或太复杂的 DOM 操作,原生 JS,足矣
解放生产力的先导阶段:jQuery 时期
时代的浪潮滚滚向前,人们很快就不再满足于简单到有些无聊的交互效果,开始追求更加丰富的用户体验,与之而来的就是大量 DOM 操作需求带来的前端开发工作量的激增。在这个过程中,早期前端们渐渐地明白了一个道理:原生 JS 提供的 DOM API,实在是太太太太太难用了。
为了能够实现高效的开发,jQuery 首先解决的就是"API 不好使"这个问题------它将 DOM API 封装为了相对简单和优雅的形式,同时一口气做掉了跨浏览器的兼容工作,并且提供了链式 API 调用、插件扩展等一系列能力用于进一步解放生产力。最终达到的效果正是我们喜闻乐见的"写得更少,做得更多"。
jQuery 使 DOM 操作变得简单、快速,并且始终确保其形式稳定、可用性稳定。虽然现在看来并不完美,但在当年能够一统江湖,确实当之无愧
民智初启:早期模板引擎方案
jQuery 帮助我们能够以更舒服的姿势操作 DOM,但它并不能从根本上解决 DOM 操作量过大情况下前端侧的压力
由于模板引擎更倾向于点对点解决烦琐 DOM 操作的问题,它在能力和定位上既不能够、也不打算替换掉 jQuery,两者是和谐共存的。因此这里不存在"模板引擎时期",只有"模板引擎方案"。
怎么理解模板这个概念呢?我们来看一个例子。比如说我现在手里有一套员工数据,数据内容如下:
js
const staff = [
{
name: "xxx",
career: "前端",
},
{
name: "xxx",
career: "编辑",
},
{
name: "xxx",
career: "运营",
},
];
现在我想要在前端用表格展示这一堆数据,我就可以遵循模板的语法,把它塞进模板(template)里去。下面就是一个典型的模板语法使用示例:
js
<table>
{% staff.forEach(function(person){ %}
<tr>
<td>{% student.name %}</td>
<td>{% student.age %}</td>
</tr>
{% }); %}
</table>
可以看出,模板语法其实就是把 JS 和 HTML 结合在一起的一种规则,而模板引擎做的事情也非常容易理解。
把 staff 这个数据源读进去,塞到预置好的 HTML 模板里,然后把两者融合在一起,吐出一段目标字符串给你。这段字符串的内容,其实就是一份标准的、可用于渲染的 HTML 代码,它将对应一个 DOM 元素。最后,将这个 DOM 元素挂载到页面中去,整个模板的渲染流程也就走完了
这个过程可以用伪代码来表示,如下所示:
js
// 数据和模板融合出 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 确实在思想上存在递进关系
虽然指望模板引擎实现生产力解放有些天方夜谭,但模板引擎在思想上无疑具备高度的先进性:允许程序员只关心数据而不必关心 DOM 细节的这一操作,和 React 的"数据驱动视图"思想如出一辙,实在是高!
虚拟 DOM 的价值
在整个 DOM 操作的演化过程中,主要矛盾并不在于性能,而在于开发者写得爽不爽,在于研发体验/研发效率。虚拟 DOM 不是别的,正是前端开发们为了追求更好的研发体验和研发效率而创造出来的高阶产物
虚拟 DOM 并不一定会带来更好的性能,React 官方也从来没有把虚拟 DOM 作为性能层面的卖点对外输出过。虚拟 DOM 的优越之处在于,它能够在提供更爽、更高效的研发模式(也就是函数式的 UI 编程方式)的同时,仍然保持一个还不错的性能
性能问题属于前端领域复杂度比较高的问题。当我们量化性能的时候,往往并不能只追求一个单一的数据,而是需要结合具体的参照物、渲染的阶段、数据的吞吐量等各种要素来作分情况的讨论。
拿前面讲过的模板渲染来举例,我们可以对比一下它和虚拟 DOM 在性能开销上的差异。两者的渲染工作流对比如下图所示
从图中可以看出,模板渲染的步骤 1,和虚拟 DOM 渲染的步骤 1、2 都属于 JS 范畴的行为,这两者是具备可比性的,我们放在一起来看:动态生成 HTML 字符串的过程本质是对字符串的拼接,对性能的消耗是有限的;而虚拟 DOM 的构建和 diff 过程逻辑则相对复杂,它不可避免地涉及递归、遍历等耗时操作。因此在 JS 行为这个层面,模板渲染胜出
模板渲染的步骤 2,和虚拟 DOM 的步骤 3 都属于 DOM 范畴的行为,两者具备可比性,因此我们仍然可以愉快地对比下去:模板渲染是全量更新,而虚拟 DOM 是差量更新
乍一看好像差量更新一定比全量更新高效,但你需要考虑这样一种情况:数据内容变化非常大(或者说整个发生了改变),促使差量更新计算出来的结果和全量更新极为接近(或者说完全一样)
在这种情况下,DOM 更新的工作量基本一致,而虚拟 DOM 却伴随着开销更大的 JS 计算,此时会出现的一种现象就是模板渲染和虚拟 DOM 在整体性能上难分伯仲:若两者最终计算出的 DOM 更新内容完全一致,那么虚拟 DOM 大概率不敌模板渲染;但只要两者在最终 DOM 操作量上拉开那么一点点的差距,虚拟 DOM 就将具备战胜模板渲染的底气。因为虚拟 DOM 的劣势主要在于 JS 计算的耗时,而 DOM 操作的能耗和 JS 计算的能耗根本不在一个量级,极少量的 DOM 操作耗费的性能足以支撑大量的 JS 计算
上面讨论的这种情况相对来说比较极端。在实际的开发中,更加高频的场景是这样的:我每次 setState 的时候只修改少量的数据,比如一个对象中的某几个属性,再比如一个数组中的某几个元素。在这样的场景下,模板渲染和虚拟 DOM 之间 DOM 操作量级的差距就完全拉开了,虚拟 DOM 将在性能上具备绝对的优势。
虚拟 DOM 解决的关键问题有以下两个:
- 研发体验/研发效率的问题:这一点前面已经反复强调过,DOM 操作模式的每一次革新,背后都是前端对效率和体验的进一步追求。虚拟 DOM 的出现,为数据驱动视图这一思想提供了高度可用的载体,使得前端开发能够基于函数式 UI 的编程方式实现高效的声明式编程
- 跨平台的问题:虚拟 DOM 是对真实渲染内容的一层抽象。若没有这一层抽象,那么视图层将和渲染平台紧密耦合在一起,为了描述同样的视图内容,你可能要分别在 Web 端和 Native 端写完全不同的两套甚至多套代码。但现在中间多了一层描述性的虚拟 DOM,它描述的东西可以是真实 DOM,也可以是iOS 界面、安卓界面、小程序......同一套虚拟 DOM,可以对接不同平台的渲染逻辑,从而实现"一次编码,多端运行",如下图所示。其实说到底,跨平台也是研发提效的一种手段,它在思想上和1是高度呼应的
除了差量更新以外,"批量更新"也是虚拟 DOM 在性能方面所做的一个重要努力,在差量更新速度非常快的情况下(比如极短的时间里多次操作同一个 DOM),用户实际上只能看到最后一次更新的效果。这种场景下,前面几次的更新动作虽然意义不大,但都会触发重渲染流程,带来大量不必要的高耗能操作,这时就需要请 batch 来帮忙了,batch 的作用是缓冲每次生成的补丁集,它会把收集到的多个补丁集暂存到队列中,再将最终的结果交给渲染函数,最终实现集中化的 DOM 批量更新
React中的虚拟DOM
在 JSX 的使用中,JSX 所描述的结构,会转译成 React.createElement 函数,React 会持有一棵虚拟 DOM 树(其实有两颗),在状态变更后,会触发虚拟 DOM 树的修改,再以此为基础修改真实 DOM
基于基本认知,React 有两个函数:
- diff 函数,去计算状态变更前后的虚拟 DOM 树差异;
- 渲染函数,渲染整个虚拟 DOM 树或者处理差异点
正是由于计算与渲染的分工,才会有React 与 ReactDOM 是两个库,其中 React 主要的工作是组件实现、更新调度等计算工作;而 ReactDOM 提供了在网页上渲染的基础。
也正因为这样的拆分,当 React 向 iOS、Android 开发时,只需要通过 React Native 提供 Native 层的元素渲染即可完成
优点
虚拟DOM有哪些优点呢?
- 性能优越
- 规避 XSS
- 可跨平台
但这样的答案是有问题的。因为在谈论优势时一定要讨论它的边界
虚拟 DOM 一定比真实的 DOM 操作性能更高吗?其实不是,如果只修改一个按钮的文案,那么虚拟 DOM 的操作无论如何都不可能比真实的 DOM 操作更快。所以一定要回到具体的场景进行探讨
如果大量的直接操作 DOM 则容易引起网页性能的下降,这时 React 基于虚拟 DOM 的 diff 处理与批处理操作,可以降低 DOM 的操作范围与频次,提升页面性能。在这样的场景下虚拟 DOM 就比较快,那什么场景下虚拟 DOM 慢呢?首次渲染或微量操作,虚拟 DOM 的渲染速度就会比真实 DOM 更慢
那虚拟 DOM 一定可以规避 XSS吗?虚拟 DOM 内部确保了字符转义,所以确实可以做到这点,但 React 存在风险,因为 React 留有 dangerouslySetInnerHTML API 绕过转义
没有虚拟 DOM 不能实现跨平台吗?比如 NativeScript 没有虚拟 DOM 层 ,它是通过提供兼容原生 API 的 JS API 实现跨平台开发。那虚拟 DOM 的优势在哪里?实际上它的优势在于跨平台的成本更低。在 React Native 之后,前端社区从虚拟 DOM 中体会到了跨平台的无限前景,所以在后续的发展中,都借鉴了虚拟 DOM。比如:社区流行的小程序同构方案,在构建过程中会提供类似虚拟 DOM 的结构描述对象,来支撑多端转换
缺点
社区公认虚拟 DOM 的缺点有两个:
-
内存占用较高。因为当前网页的虚拟 DOM 包含了真实 DOM 的完整信息,而且由于是 Object,其内存占用肯定会有所上升。
-
无法进行极致优化。 虽然虚拟 DOM 足以应对绝大部分应用的性能需求,但在一些性能要求极高的应用中,虚拟 DOM 无法进行针对性的极致优化,比如实现类似 Google Earth 的场景