1.3 虚拟DOM

我们已经讨论了一些React的高级特性。我认为React可以帮助研发团队更好地创建用户界面,并且这得益于React提供的思维模型和API。所有这一切背后隐藏着什么?React的主旨是推动简化复杂的任务并把不必要的复杂性从开发人员身上抽离出来。React试图将性能做得恰到好处,从而让研发人员腾出时间思考应用的其他方面。它这么做的主要方式之一就是鼓励开发人员使用声明式编程而不是命令式编程。开发人员要声明组件在不同状态下的行为和外观,而React的内部机制处理管理更新、更新UI以反映更改等的复杂性。

驱动这些的主要技术之一就是虚拟DOM。这种虚拟DOM是模仿或镜像存在于浏览器中的文档对象模型的数据结构或数据结构的集合。我之所以说“这种虚拟DOM”,是因为其他像Ember这样的框架采用了它们自己的类似技术的实现。通常,虚拟DOM会作为应用程序代码和浏览器DOM之间的中间层。虚拟DOM向开发人员隐藏了变更检测与管理的复杂性并将其转移到专门的抽象层。在接下来的小节中,我们将从更高层次来了解它是如何在React中起作用的。图1-3展示了DOM和虚拟DOM之间的关系,我们稍后将对此进行讨论。

图1-3 DOM和虚拟DOM。React的虚拟DOM处理数据的变更检测并将浏览器事件转换为React组件可以理解和响应的事件。React的虚拟DOM还为性能专门优化了对DOM的更新操作

1.3.1 DOM

确保我们理解React的虚拟DOM的最佳途径就是首先检查我们对DOM的理解。如果觉得自己对DOM已经了然于心,可以选择跳过这部分。但如果不是,让我们从一个重要的问题开始:什么是DOM?DOM或文档对象模型是一个允许JavaScript程序与不同类型的文档(HTML、XML和SVG)进行交互的编程接口。它有标准驱动的规范,这意味着公共工作组已经建立了它应该具有的标准特性集以及行为方式。虽然存在其他实现,但是DOM几乎已经是Chrome、Firefox和Edge等Web浏览器的代名词了。

DOM提供了访问、存储和操纵文档不同部分的结构化方式。从较高层面来讲,DOM是一种反映了XML文档层次结构的树形结构。这个树结构由子树组成,子树由节点组成。这些(节点)是组成Web页面和应用的div和其他元素。

人们之前可能使用过DOM API——但他们没有意识到自己正在使用它。每当使用JavaScript中的方法访问、修改或者存储一些HTML文档相关的信息时,几乎可以肯定,人们就是在使用DOM或DOM相关的API。这意味着,你在JavaScript中使用的方法不全是JavaScript语言本身的一部分(document.findElemenyById、querySelectorAll、alert等)。它们是更大的Web API集合(浏览器中的DOM和其他API)的一部分,这些API让人们能够与文档交互。图1-4展示了可能在Web页面中看到的DOM树结构的简化版本。

图1-4 这是DOM树结构的简化版本,使用人们熟悉的元素。暴露给JavaScript的DOM API允许对树中的这些元素执行操作

用来更新或查询Web页面的常见方法或属性有getElementById、parent.appendChild、querySelectorAll、innerHTML等。这些接口都是由宿主环境(这里指的是浏览器)提供的并允许JavaScript与DOM交互。没有这些能力,我们就没有那么有趣的Web应用可用了,也可能没有关于React的书可写了。

与DOM交互通常很简单,但在大型Web应用中可能会变得复杂。幸运的是,当使用React构建应用时我们通常不需要直接与DOM交互——我们基本上把它都交给了React。有些场景下我们可能希望绕过虚拟DOM直接与DOM交互,我们将在后面的几章对此进行讨论。

1.3.2 虚拟DOM

浏览器中的Web API让我们可以使用JavaScript通过DOM与Web文档进行交互。但如果我们已经能做到这一点,为什么在这之间还需要别的东西?首先我想说明的是,React实现虚拟DOM并不意味着常规Web API不好或者不如React好。没有它们,React就不能工作。然而,在更大的Web应用中直接使用DOM的确有一些痛点,通常这些痛点出现在变更检测方面。当数据变化时,我们希望通过更新UI来反映它,但很难以一种有效且易于理解的方式做到这点,所以React致力于解决这个问题。

出现这个问题的部分原因是浏览器处理与DOM交互的方式。当访问、修改或创建DOM元素时,浏览器常常要在一个结构化的树上执行查询来找到指定的元素。这只是访问一个元素,而且通常仅是更新的第一部分。通常情况下,作为更改的一部分,它不得不重新进行布局、缩放和其他操作——所有这些操作往往计算量都很大。虚拟DOM也无法绕过这个问题,但它可以在这些限制下帮助优化对DOM的更新。

当创建和管理一个处理随时间变化的数据的大型应用时,可能需要对DOM进行许多更改,这些更改通常会发生冲突或以不太理想的方式完成。这可能会导致一个过于复杂的系统,这个系统不但对工程师来说难以使用而且可能会导致用户体验不佳——这是“双输”。因此,性能是React设计和实现的另一个关键考虑因素。实现虚拟DOM有助于解决这个问题,但应该注意的是,它设计得只是“够快”而已。React的虚拟DOM更为重要的是提供了健壮的API、简单的思维模型和诸如跨浏览器兼容性等其他特性,而不是对性能的极端关注。我之所以强调这一点是因为人们可能听说虚拟DOM是某种“性能银弹”。它确实是高性能的,但它并不是神奇的“性能银弹”,最后我想说的是,React的许多其他好处对于使用React更为重要。

1.3.3 更新与差异比对

虚拟DOM是如何工作的?React的虚拟DOM与另一个软件世界有一些共同点——3D游戏。3D游戏有时会使用一个渲染过程,其工作原理大致如下:从游戏服务器获取信息,将信息发送到游戏世界(用户看到的视觉表现),确定需要对虚拟世界进行哪些更改,最后让显卡决定所需的最小更改。这种方法的一个优点是,只需要一些资源来处理增量更新,这种更新方式通常比全部更新快得多。

这是对3D游戏渲染和更新方式的粗略描述,当审视React执行更新的方式时,这个基本思想为我们提供了一个很好的参考。DOM变更做得不好的话代价可能会很大,所以React试图在更新UI方面更有效率并采用了类似3D游戏的方法。

如图1-5所示,React在内存中创建并维护了一个虚拟DOM,并且一个像React-DOM这样的渲染器基于更改对浏览器DOM进行更新。React可以执行智能更新并且只更新已更改的部分,因为它可以使用启发式对比来计算内存DOM的哪些部分需要更新到DOM。理论上讲,这比“脏检查”或其他更暴力的方法更加简洁优雅,但主要的实践意义是,开发者可以少考虑很多复杂的状态追踪。

图1-5 React的对比和更新流程。当改变发生时,React确定实际DOM和内存DOM的差异,然后对浏览器DOM执行高效更新。这个过程通常被称为diff(什么改变了)和patch(只更新改变的东西)过程

1.3.4 虚拟DOM:渴求速度

正如我所指出的,虚拟DOM有很多比速度更重要的东西。它通过设计得到高性能,并且通常会产生简单快速的应用,这样的速度对于现代Web应用的需要已经足够快了。工程师们对性能和更好的思维模型如此欣赏,以至于许多流行的JavaScript库都在创建自己版本的虚拟DOM或者变体的虚拟DOM。即使是在这些情况下,人们也倾向于认为虚拟DOM主要关注的是性能。性能是React的关键特性,但与简单相比较,它却是次要的。虚拟DOM一定程度上能够让开发人员推迟思考复杂的状态逻辑并专注于应用中其他更重要的部分。总而言之,速度和简单意味着更快乐的用户和更快乐的开发者——双赢!

我花了些时间来讨论虚拟DOM,但我并不想让人觉得它是使用React的重要部分。实际上,人们不需要过多考虑虚拟DOM是如何完成数据更新或如何对应用进行更改的。这正是React简单的地方:人们被解放出来去关注应用中最需要关注的那些部分。