TAT.李强 React 虚拟 DOM 浅析
In Web开发 on 2015年10月31日 by view: 19,834
16

       在 Web 开发中,需要将数据的变化实时反映到 UI 上,这时就需要对 DOM 进行操作,但是复杂或频繁的 DOM 操作通常是性能瓶颈产生的原因,为此,React 引入了虚拟 DOM(Virtual DOM)的机制。

  1. 什么是虚拟 DOM?
  2. 虚拟 DOM VS 直接操作原生 DOM?
  3. 虚拟 DOM VS MVVM?
  4. 对 React 虚拟 DOM 的误解?


一、什么是虚拟 DOM?

        在 React 中,render 执行的结果得到的并不是真正的 DOM 节点,结果仅仅是轻量级的 JavaScript 对象,我们称之为 virtual DOM。

        虚拟 DOM 是 React 的一大亮点,具有 batching(批处理) 和高效的 Diff 算法。这让我们可以无需担心性能问题而” 毫无顾忌” 的随时“ 刷新” 整个页面,由虚拟 DOM 来确保只对界面上真正变化的部分进行实际的 DOM 操作。在实际开发中基本无需关心虚拟 DOM 是如何运作的,但是理解其运行机制不仅有助于更好的理解 React 组件的生命周期,而且对于进一步优化 React 程序也会有很大帮助。

二、虚拟 DOM VS 直接操作原生 DOM?

       如果没有 Virtual DOM,简单来说就是直接重置 innerHTML。这样操作,在一个大型列表所有数据都变了的情况下,还算是合理,但是,当只有一行数据发生变化时,它也需要重置整个 innerHTML,这时候显然就造成了大量浪费。

比较 innerHTML 和 Virtual DOM 的重绘过程如下:

innerHTML: render html string + 重新创建所有 DOM 元素

Virtual DOM: render Virtual DOM + diff + 必要的 DOM 更新

        和 DOM 操作比起来,js 计算是非常便宜的。Virtual DOM render + diff 显然比渲染 html 字符串要慢,但是,它依然是纯 js 层面的计算,比起后面的 DOM 操作来说,依然便宜了太多。当然,曾有人做过验证说 React 的性能不如直接操作真实 DOM,代码如下:

        该测试用例中虽然构造了一个包含 1000 个 Tag 的 String,并把它添加到 DOM 树中,但是只做了一次 DOM 操作。然而,在实际开发过程中,这 1000 个元素更新可能分布在 20 个逻辑块中,每个逻辑块中包含 50 个元素,当页面需要更新时,都会引起 DOM 树的更新,上述代码就近似变成了如下格式:

         这样来看,React 的性能就远胜于原生 DOM 操作了。

        而且,DOM 完全不属于 Javascript (也不在 Javascript 引擎中存在).。Javascript 其实是一个非常独立的引擎,DOM 其实是浏览器引出的一组让 Javascript 操作 HTML 文档的 API 而已。在即时编译的时代,调用 DOM 的开销是很大的。而 Virtual DOM 的执行完全都在 Javascript 引擎中,完全不会有这个开销。

        React.js 相对于直接操作原生 DOM 有很大的性能优势, 很大程度上都要归功于 virtual DOM 的 batching 和 diff。batching 把所有的 DOM 操作搜集起来,一次性提交给真实的 DOM。diff 算法时间复杂度也从标准的的 Diff 算法的 O(n^3) 降到了 O(n)。这里留到下一次博客单独讲。

三、虚拟 DOM VS MVVM?

         相比起 React,其他 MVVM 系框架比如 Angular, Knockout 以及 Vue、Avalon 采用的都是数据绑定:通过 Directive/Binding 对象,观察数据变化并保留对实际 DOM 元素的引用,当有数据变化时进行对应的操作。MVVM 的变化检查是数据层面的,而 React 的检查是 DOM 结构层面的。MVVM 的性能也根据变动检测的实现原理有所不同:Angular 的脏检查使得任何变动都有固定的 O(watcher count) 的代价;Knockout/Vue/Avalon 都采用了依赖收集,在 js 和 DOM 层面都是 O(change):

  • 脏检查:scope digest + 必要 DOM 更新
  • 依赖收集:重新收集依赖 + 必要 DOM 更新

        可以看到,Angular 最不效率的地方在于任何小变动都有的和 watcher 数量相关的性能代价。但是!当所有数据都变了的时候,Angular 其实并不吃亏。依赖收集在初始化和数据变化的时候都需要重新收集依赖,这个代价在小量更新的时候几乎可以忽略,但在数据量庞大的时候也会产生一定的消耗。
        MVVM 渲染列表的时候,由于每一行都有自己的数据作用域,所以通常都是每一行有一个对应的 ViewModel 实例,或者是一个稍微轻量一些的利用原型继承的 "scope" 对象,但也有一定的代价。所以,MVVM 列表渲染的初始化几乎一定比 React 慢,因为创建 ViewModel / scope 实例比起 Virtual DOM 来说要昂贵很多。这里所有 MVVM 实现的一个共同问题就是在列表渲染的数据源变动时,尤其是当数据是全新的对象时,如何有效地复用已经创建的 ViewModel 实例和 DOM 元素。假如没有任何复用方面的优化,由于数据是 “ 全新” 的,MVVM 实际上需要销毁之前的所有实例,重新创建所有实例,最后再进行一次渲染!这就是为什么题目里链接的 angular/knockout 实现都相对比较慢。相比之下,React 的变动检查由于是 DOM 结构层面的,即使是全新的数据,只要最后渲染结果没变,那么就不需要做无用功。
        Angular 和 Vue 都提供了列表重绘的优化机制,也就是 “ 提示” 框架如何有效地复用实例和 DOM 元素。比如数据库里的同一个对象,在两次前端 API 调用里面会成为不同的对象,但是它们依然有一样的 uid。这时候你就可以提示 track by uid 来让 Angular 知道,这两个对象其实是同一份数据。那么原来这份数据对应的实例和 DOM 元素都可以复用,只需要更新变动了的部分。或者,你也可以直接 track by $index 来进行 “ 原地复用”:直接根据在数组里的位置进行复用。在题目给出的例子里,如果 angular 实现加上 track by $index 的话,后续重绘是不会比 React 慢多少的。甚至在 dbmonster 测试中,Angular 和 Vue 用了 track by $index 以后都比 React 快: dbmon (注意 Angular 默认版本无优化,优化过的在下面)

        在比较性能的时候,要分清楚初始渲染、小量数据更新、大量数据更新这些不同的场合。Virtual DOM、脏检查 MVVM、数据收集 MVVM 在不同场合各有不同的表现和不同的优化需求。Virtual DOM 为了提升小量数据更新时的性能,也需要针对性的优化,比如 shouldComponentUpdate 或是 immutable data。

  • 初始渲染:Virtual DOM > 脏检查 >= 依赖收集
  • 小量数据更新:依赖收集 >> Virtual DOM + 优化 > 脏检查(无法优化)> Virtual DOM 无优化
  • 大量数据更新:脏检查 + 优化 >= 依赖收集 + 优化 > Virtual DOM(无法/无需优化)>> MVVM 无优化

(该段落借鉴了知乎的相关回答)

四、对 React 虚拟 DOM 的误解?

        React 从来没有说过 “React 比原生操作 DOM 快”。React 给我们的保证是,在不需要手动优化的情况下,它依然可以给我们提供过得去的性能。

        React 掩盖了底层的 DOM 操作,可以用更声明式的方式来描述我们目的,从而让代码更容易维护。下面还是借鉴了知乎上的回答:没有任何框架可以比纯手动的优化 DOM 操作更快,因为框架的 DOM 操作层需要应对任何上层 API 可能产生的操作,它的实现必须是普适的。针对任何一个 benchmark,我都可以写出比任何框架更快的手动优化,但是那有什么意义呢?在构建一个实际应用的时候,你难道为每一个地方都去做手动优化吗?出于可维护性的考虑,这显然不可能。

原创文章转载请注明:

转载自AlloyTeam:http://www.alloyteam.com/2015/10/react-virtual-analysis-of-the-dom/

  1. 颜海镜 2017 年 10 月 31 日

    学习

  2. monkindey 2016 年 6 月 24 日

    这篇不是 https://www.zhihu.com/question/31809713 知乎上说的么?

  3. aaron 2015 年 11 月 30 日

    大概是怎么 diff 的,以及 diff 后怎么更新到 dom 上的 。这最重要的地方没讲到

    • TAT.李强

      TAT.李强 2015 年 12 月 14 日

      是的,这里东西比较多,下次单独讲,文中提了的,还望继续关注。

  4. yimity 2015 年 11 月 10 日

    这里还是没有说清楚具体 Virtual DOM diff 之后是如何将这些 dom 更新到 ui 中的。
    例如有个例子:有 1000 个 li ,每个 li 里面有三项 a p
    span(具体是什么不重要)内容,那我如果更新某一个 li 里面的某一项 a 的内容的话,可能 react 快于 angular。但是假如我 1000 个 li 里面都有一项 p 被更新了,这时候,具体 react 是如何处理的?如果是单独更新每一个变化项 那么 react 要最终会操作 1000 次 dom 而 angular 可能只有一次。
    求解释。

    • TAT.李强

      TAT.李强 2015 年 12 月 14 日

      是的,关于 diff 和 batching 下次会单独讲一下,希望继续关注哈。先结合你这个例子说一下哈,在列表项发生变化的时候,react 会先和上一次缓存的 virtual dom 做 diff,这个过程会计算出从现有的真实 dom 到新的真实 dom 需要的最少操作,然后通过 dom api 作用于现在的真实 dom 上,渲染得到新的 dom。对该例而言,和代码实现也有关系,diff 有三个先验条件,对于组件树只比较同层级的、基于组件、基于属性 key。如果对每个列表项都设置了唯一的 key,那么确实是要有 1000 次 dom 操作,就像你说的,肯定性能不好,而如果代码实现上是基于组件的,那么 react 就会直接移除整个组件,不会对组件内部元素做 diff,这样也是执行了一次删除一次重建,但是这样实现 react 并不支持。所以对于一次更新一个大列表项来说,react 的性能并不算好。对于 angular 来说,也和代码实现有关,他也并不是只有一次 dom 操作那么简单,angular 建议一个页面最多 2000 个双向绑定。不知道有没有说清楚,下次会分享 diff 的相关问题,希望看过后再给意见。其实个人感觉 react 最大的优点并不是它的性能怎么样,而是它组件化、模块化的思想,对前端代码的规范性做出了巨大贡献,降低了多人维护成本。

  5. react新手 2015 年 11 月 3 日

    web 工程师们不是都说 avalon 远超 angular 吗?应该用 avalon 与 react 对比看看

    • 亦驰fantasy 2015 年 11 月 12 日

      我怎么没听说过…

      • react黑 2015 年 11 月 25 日

        你去百度看看

        • avalon新手 2015 年 11 月 25 日

          avalon 作者是这样称赞 react 的:在 View 减少冗余的 DOM 操作,必须减少一次事务中对同一个元素的重复操作,Angular 有 $apply 手动触发,avalon 可以用 $unwatch、$watch,但都不太好用。这难题最后被 Facebook 的新锐视图库 React 搞定了,它号称是使用了一种叫 Virtual DOM 的技术搞定。显然,这答案没有暴露其全貌,其他使用 Virtual DOM 的库,性能也很难追得上 React。

          React 为了提高性能,其最核心的架子是其基于层次结构的 UUID 技术。有了它,才能实现节点的最小化更新。比如,一个对象里面有 30 个键值对,后来更新该对象,换成另外 11 个新的键值对,这样我们只要去掉页面多出的 19 个 DOM 节点,再修改已有 11 个节点内容或属性即可。如果你的框架使用静态模板来实现这一功能,这 30 个节点需要重新创建与插入,性能肯定也没有这么好。

    • TAT.李强

      TAT.李强 2015 年 12 月 14 日

      并不能说哪个框架优于哪个框架,上文也是以框架的实现原理进行对比的,并不是说拿 angular 或者 avalon 与 react 对比,而是以底层实现机制做的对比,涵盖了 angular 和 avalon。在不同场景下,每个框架的表现也不尽相同,所以不能一棒子说死,说哪一个更好。

  6. 我这是好的 2015 年 11 月 3 日

    为啥光比渲染速度,不比内存占用呢?

    • TAT.李强

      TAT.李强 2015 年 12 月 14 日

      这个问题提的好,下次会在 diff 的时候一起说一下。其实我们都明白的是,浏览器渲染 dom 是最耗内存的,react 引入 virtual dom 来减少内存的开支,在 js engine 层面先进行 diff,计算出从现有的 dom 到更改后的 dom 需要的最小操作(这里都是 js 层面的),在通过 dom api 作用于现有的 dom 上,渲染得到新的 dom。可以说 react 的推出,也是 facebook 工程师们出于节省内存的考虑。

      • yan.O 2016 年 6 月 27 日

        这个也未必,比 mvvm 少

  7. 泽恺 2015 年 11 月 2 日

    正如上文中的一句话:“如果 angular 实现加上 track by $index 的话,后续重绘是不会比 React 慢多少的”。所以盲目的说 React 比 Angular 快的人是不理智的啊

发表评论