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

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

    • 是的,关于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最大的优点并不是它的性能怎么样,而是它组件化、模块化的思想,对前端代码的规范性做出了巨大贡献,降低了多人维护成本。

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

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

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

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

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