为什么React的虚拟DOM概念被认为比脏模型检查更具性能?

395

我看了一个React开发者的演讲(Pete Hunt: React: Rethinking best practices -- JSConf EU 2013),其中演讲者提到模型的脏检查可能会很慢。但是,计算虚拟DOM之间的差异实际上是否更少效率,因为在大多数情况下,虚拟DOM应该比模型更大?

我非常喜欢虚拟DOM的潜力(特别是服务器端渲染),但我想知道所有的优缺点。


我认为你也可以提到这个演讲 https://www.youtube.com/watch?v=-DX3vJiqxm4,他在其中专门谈到了基准测试。 - inafalcao
6个回答

511

我是virtual-dom模块的主要作者,所以也许可以回答你的问题。实际上有两个需要解决的问题:

  1. 何时重新渲染?答案:当我观察到数据是脏的时候。
  2. 如何高效地重新渲染?答案:使用虚拟DOM生成真实DOM补丁。

在React中,每个组件都有一个状态。这个状态就像你可能在knockout或其他MVVM样式库中找到的可观察对象一样。基本上,React知道何时重新渲染场景,因为它能够观察到这些数据何时发生了变化。脏检查比观察对象慢,因为您必须定期轮询数据并递归检查数据结构中的所有值。相比之下,设置状态上的值将向侦听器发出信号,表示某些状态已更改,因此React可以简单地侦听状态上的更改事件并排队重新渲染。

虚拟DOM用于DOM的高效重新渲染。这与检查数据的脏不相关。您可以使用虚拟DOM进行重新渲染,无论是否进行脏检查。在计算两个虚拟树之间的差异方面确实存在一些开销,但虚拟DOM差异是关于理解DOM中需要更新的内容,而不是您的数据是否已更改。事实上,差异算法本身就是一个脏检查器,但它用于检查DOM是否是脏的。

我们的目标是仅在状态更改时重新呈现虚拟树。因此,使用可观察对象来检查状态是否更改是防止不必要的重新渲染的有效方法,这将导致许多不必要的树差异。如果没有任何更改,我们就什么也不做。

虚拟DOM非常好,因为它让我们的代码看起来像是在重新渲染整个场景。在后台,我们想要计算一个补丁操作,以便更新DOM并使其看起来符合我们的期望。因此,尽管虚拟DOM的差异/补丁算法可能不是最优解决方案,但它为我们提供了一种非常好的方式来表达我们的应用程序。我们只需声明确切地想要什么,React/virtual-dom 将会找出如何使您的场景看起来像这样。我们不必手动进行DOM操作或混淆之前的DOM状态。我们也不必重新渲染整个场景,这可能比修补它更低效。


1
React在组件属性上进行脏检查吗?我问这个问题是因为没有setProps()函数。 - bennlich
2
这里有一个 setProps 方法:http://facebook.github.io/react/docs/component-api.html#setprops - Marius
2
这样的“不必要的重新渲染”的例子是什么? - vsync
10
当你说“虚拟DOM的差异/补丁算法可能不是最优解”的时候,你是否有想到一个理论上更优的解决方案? - CMCDragonkai
3
这似乎并没有完全回答问题。React要求您使用setState来发出状态更改的信号。如果您能够执行“this.state.cats = 99”,仍然需要进行脏检查来检查模型更改,就像Angular会对$scope树进行脏检查一样。这不是比较两种技术速度的问题,只是说React没有脏检查,因为它有一个类似Backbone的setter方法。 - superluminary
显示剩余6条评论

135

我最近阅读了一篇关于React的差异算法的详细文章,链接在这里:http://calendar.perfplanet.com/2013/diff/。从我的理解来看,React之所以快速是因为:

  • 批量DOM读写操作。
  • 仅更新子树的高效算法。

与脏检查相比,我认为主要的区别有:

  1. 模型脏检查:每当调用setState时,React组件都会被明确地设置为脏,所以这里不需要进行(数据)比较。而对于脏检查,(模型)比较始终会在每个循环周期内发生。

  2. DOM更新:修改DOM也会应用和计算CSS样式、布局,因此DOM操作非常昂贵。无需修改DOM可节省的时间可能比虚拟DOM的差异计算所花费的时间更长。

对于具有大量字段或大型列表等复杂模型来说,第二点甚至更加重要。复杂模型的一个字段更改将导致仅涉及该字段的DOM元素的操作,而不是整个视图/模板。


1
实际上,我也读了一些文章,所以我现在(至少大致上)知道它是如何工作的,我只是想弄清楚为什么它比模型的脏检查更有效。1)是的,它不会比较模型,但会比较更大的虚拟DOM 2)模型的脏检查使我们能够像Angular一样只更新所需的内容。 - Daniil
我相信只有与更改的组件对应的虚拟DOM部分需要进行比较,而脏检查会在每个digest循环中发生,针对每个作用域中的每个值,即使没有任何更改。如果大量数据发生变化,则虚拟DOM将不太有效,但对于小数据更改则不会。 - tungd
1
说到Angular,因为监视器在digest期间也可以改变状态,所以$scope.$digest在每个digest周期中会被执行多次,这意味着与单次部分虚拟DOM树比较相比,需要进行多次完整数据比较。 - tungd
4
很遗憾,许多聪明的开发者发明了很多技巧来应对“缓慢”的DOM等问题,而不是将我们的注意力集中在修复浏览器本身上,一劳永逸地摆脱DOM的缓慢。这就像利用全人类的资源研究如何处理癌症并改善患者的生活,而不是直接治愈癌症本身。荒谬。 - vsync
还有,听起来不浪费精力去治疗不可治愈的疾病似乎是个好主意,因为它是细胞工作的基本副作用。就像看起来很明智不要试图解决CSS设计中的基本功能问题一样。另外,想得更难并不能让问题消失。 - Nick Bailey
显示剩余4条评论

76

我非常喜欢虚拟DOM的潜力(尤其是服务器端渲染),但我想知道所有的优缺点。

-- 发帖者

React并不是唯一的DOM操作库。我鼓励你通过阅读这篇来自Auth0的文章(链接在此)来了解其他替代方案,其中包含详细的解释和基准测试。正如你所问的,我将在这里列出它们的优缺点:

React.js的虚拟DOM

enter image description here

优点

  • 快速高效的"diffing"算法
  • 多个前端(JSX,超文本标记语言)
  • 足够轻便以在移动设备上运行
  • 有很多关注和支持
  • 可以在没有React的情况下使用(即作为独立引擎)

缺点

  • 完整的内存DOM副本(内存使用较高)
  • 没有区分静态和动态元素

Ember.js的Glimmer

enter image description here

优点

  • 快速高效的diffing算法
  • 区分静态和动态元素
  • 与Ember的API完全兼容(即使不对现有代码进行重大更新也能获得好处)
  • 轻量级内存中的DOM表示

缺点

  • 仅适用于Ember
  • 仅有一个前端可用

增量DOM

enter image description here

优点

  • 减少内存使用
  • 简单的API
  • 易于与许多前端和框架集成(从一开始就是作为模板引擎后端而设计的)

缺点

  • 不如其他库快(这是有争议的,请参见下面的基准测试结果)
  • 使用较少,社区普及度较低

ReactJS的DOM操作似乎有些不太对劲。ReactJS的虚拟DOM是完全改变的,而不是实际的DOM - 对吗?我正在查看原始文章所引用的文章,这是我看到的 - https://teropa.info/images/onchange_vdom_change.svg。https://teropa.info/blog/2015/03/02/change-and-its-detection-in-javascript-frameworks.html - smile.al.d.way

36
以下是React团队成员Sebastian Markbåge的评论,阐明了一些内容:
React在输出上进行差分(这是一种已知的可序列化格式,即DOM属性)。这意味着源数据可以是任何格式。它可以是不可变的数据结构和闭包内部的状态。
Angular模型不保留引用透明度,因此本质上是可变的。您需要改变现有模型来跟踪更改。如果您的数据源是不可变的数据或每次都是新的数据结构(例如JSON响应)怎么办?
脏检查和Object.observe无法在闭包范围状态下工作。
这两件事对于函数模式非常有限制性。
此外,当您的模型复杂性增加时,脏跟踪变得越来越昂贵。但是,如果您只在视觉树上进行差分,就像React一样,那么它不会增长太多,因为您能够在屏幕上显示的数据量在任何给定时间都受到UI的限制。Pete上面的链接涵盖了更多性能方面的好处。

https://news.ycombinator.com/item?id=6937668


2
实际上关于最后一段,应该是错误的:模型比虚拟DOM更大,因为对于每个模型值,通常至少有一个虚拟DOM元素(通常还会有更多)。为什么我想要一个不显示的模型? - Daniil
2
分页缓存集合。 - kentor

0
在React中,你的每个组件都有一个状态。这个状态就像你可能在knockout或其他MVVM风格库中找到的可观察对象。本质上,当数据发生变化时,React知道何时重新渲染场景,因为它能够观察到。脏检查比观察对象慢,因为你必须在固定间隔内轮询数据并递归地检查数据结构中的所有值。相比之下,在状态上设置一个值将向侦听器发出信号,表示某些状态已经改变,因此React可以简单地侦听状态上的更改事件并排队重新呈现。虚拟DOM用于高效重新呈现DOM。这与脏数据检查不是真正相关的。使用虚拟DOM重渲染时,您可以使用脏检查或不使用脏检查。你说得对,计算两个虚拟树之间的差异存在一些开销,但是虚拟DOM差异是关于了解DOM中需要更新什么而不是你的数据是否已经改变。实际上,差异算法本身就是一个脏检查器,但它被用来查看DOM是否脏。
我们的目标是只在状态更改时重新呈现虚拟树。因此,使用可观察对象检查状态是否已更改是一种有效的方式,可以防止不必要的重新呈现,这将导致大量不必要的树差异。如果没有什么改变,我们就什么也不做。

-2

虚拟 DOM 不是由 React 发明的。它是 HTML DOM 的一部分。 它是轻量级的,与浏览器特定的实现细节分离。

我们可以将虚拟 DOM 视为 React 对 HTML DOM 的本地和简化副本。它允许 React 在这个抽象世界中进行计算,并跳过“真实”的 DOM 操作,通常是缓慢和浏览器特定的。实际上,DOM 和虚拟 DOM 之间没有太大的区别。

以下是使用虚拟 DOM 的原因(来源 ReactJS 中的虚拟 DOM):

当你执行以下代码时:
document.getElementById('elementId').innerHTML = "New Value"
以下事情会发生:
  1. 浏览器需要解析HTML
  2. 它会删除elementId的子元素
  3. 更新DOM值为新值
  4. 重新计算父元素和子元素的CSS
  5. 更新布局,即每个元素在屏幕上的确切坐标
  6. 遍历渲染树并将其绘制在浏览器显示器上
重新计算CSS和更改布局使用复杂的算法,它们会影响性能。

除了更新DOM属性,即值。它遵循一种算法。

现在,假设您直接更新DOM 10次,那么所有上述步骤都将依次运行,并且更新DOM算法将花费时间来更新DOM值。

这就是为什么真实DOM比虚拟DOM慢的原因。


3
关于这个例子,如果你直接或通过虚拟DOM修改DOM,那么最终无论哪种情况,你都是在改变DOM。 - magallanes
在这两种情况下,我们都更新了DOM。但是,在虚拟DOM的情况下,它仅更新特定的键(由React的差异算法唯一定义)字段或元素标签。而更新DOM会完全更新或刷新整个DOM。 - Hemant Nagarkoti
11
我从https://hackernoon.com/virtual-dom-in-reactjs-43a3fdb1d130看到了这篇文章。如果你不是作者,最好指出文章的来源。 - Jinggang
2
这就是为什么真实 DOM 比虚拟 DOM 慢的原因。不,先生,您是错误的。 - maelswarm

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接