浅谈React的最大亮点之虚拟DOM


Posted in Javascript onMay 29, 2018

在Web开发中,需要将数据的变化实时反映到UI上,这时就需要对DOM进行操作,但是复杂或频繁的DOM操作通常是性能瓶颈产生的原因,为此,React引入了虚拟DOM(Virtual 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,代码如下:

function Raw() {
  var data = _buildData(),
    html = "";
  ...
  for(var i=0; i<data.length; i++) {
    var render = template;
    render = render.replace("{{className}}", "");
    render = render.replace("{{label}}", data[i].label);
    html += render;
  }
  ...
  container.innerHTML = html;
  ...
}

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

function Raw() {
  var data = _buildData(), 
    html = ""; 
  ... 
  for(var i=0; i<data.length; i++) { 
    var render = template; 
    render = render.replace("{{className}}", ""); 
    render = render.replace("{{label}}", data[i].label); 
    html += render; 
    if(!(i % 50)) {
      container.innerHTML = html;
    }
  } 
  ... 
}

这样来看,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):

  1. 脏检查:scope digest + 必要 DOM 更新
  2. 依赖收集:重新收集依赖 + 必要 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。

  1. 初始渲染:Virtual DOM > 脏检查 >= 依赖收集
  2. 小量数据更新:依赖收集 >> Virtual DOM + 优化 > 脏检查(无法优化) > Virtual DOM 无优化
  3. 大量数据更新:脏检查 + 优化 >= 依赖收集 + 优化 > Virtual DOM(无法/无需优化)>> MVVM 无优化
  4. (该段落借鉴了知乎的相关回答)

四、对React虚拟DOM的误解?

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

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

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
简明json介绍
Sep 28 Javascript
JavaScript 模拟用户单击事件
Dec 31 Javascript
JavaScript创建对象的写法
Aug 29 Javascript
javascript基于DOM实现权限选择实例分析
May 14 Javascript
JavaScript学习笔记之数组求和方法
Mar 23 Javascript
移动端js图片查看器
Nov 17 Javascript
使用jquery实现的循环连续可停顿滚动实例
Nov 23 Javascript
javascript按钮禁用和启用的效果实例代码
Oct 29 Javascript
layui--js控制switch的切换方法
Sep 03 Javascript
vue.config.js中配置Vue的路径别名的方法
Feb 11 Javascript
js之ajax文件上传
May 13 Javascript
使用refresh_token实现无感刷新页面
Apr 26 Javascript
深入理解Vue Computed计算属性原理
May 29 #Javascript
javascript、php关键字搜索函数的使用方法
May 29 #Javascript
Vue路由切换时的左滑和右滑效果示例
May 29 #Javascript
Vue 组件传值几种常用方法【总结】
May 28 #Javascript
讲解vue-router之命名路由和命名视图
May 28 #Javascript
微信小程序实现图片上传功能
May 28 #Javascript
微信小程序上传图片功能(附后端代码)
Jun 19 #Javascript
You might like
PHP文本数据库的搜索方法
2006/10/09 PHP
ZF框架实现发送邮件的方法
2015/12/03 PHP
PHP 7.0.2 正式版发布
2016/01/08 PHP
php文件系统处理方法小结
2016/05/23 PHP
TNC vs BOOM BO3 第二场2.13
2021/03/10 DOTA
cssQuery()的下载与使用方法
2007/01/12 Javascript
JavaScript去掉空格的方法集合
2010/12/28 Javascript
javascript 使td内容不换行不撑开
2012/11/29 Javascript
浅析document.createDocumentFragment()与js效率
2013/07/08 Javascript
html的DOM中document对象anchors集合用法实例
2015/01/21 Javascript
js使用DOM操作实现简单留言板的方法
2015/04/10 Javascript
jquery实现Ctrl+Enter提交表单的方法
2015/07/21 Javascript
JavaScript 对象深入学习总结(经典)
2015/09/29 Javascript
纯js的右下角弹窗实例
2017/03/12 Javascript
node.js中实现kindEditor图片上传功能的方法教程
2017/04/26 Javascript
JavaScript-定时器0~9抽奖系统详解(代码)
2017/08/16 Javascript
vue cli使用绝对路径引用图片问题的解决
2017/12/06 Javascript
小程序视频或音频自定义可拖拽进度条的示例代码
2018/09/30 Javascript
JavaScript和TypeScript中的void的具体使用
2019/09/12 Javascript
vue自定义指令实现仅支持输入数字和浮点型的示例
2019/10/30 Javascript
vue中英文切换实例代码
2020/01/21 Javascript
[47:06]DOTA2上海特级锦标赛主赛事日 - 4 败者组第五轮 MVP.Phx VS EG第一局
2016/03/05 DOTA
[09:13]DOTA2-DPC中国联赛 正赛 Ehome vs Magma 选手采访 1月19日
2021/03/11 DOTA
Python学习资料
2007/02/08 Python
Python 实现12306登录功能实例代码
2018/02/09 Python
python树莓派红外反射传感器
2019/01/21 Python
解决pycharm remote deployment 配置的问题
2019/06/27 Python
python实现切割url得到域名、协议、主机名等各个字段的例子
2019/07/25 Python
CSS3 特效范例整理
2011/08/22 HTML / CSS
HTML5 canvas实现雪花飘落特效
2016/03/08 HTML / CSS
SCDKey德国:全球领先的数字游戏市场
2019/04/09 全球购物
世界上最大的冷却器制造商:Igloo Coolers
2019/07/23 全球购物
大学考试作弊检讨书
2014/01/30 职场文书
大学生活委员竞选稿
2015/11/21 职场文书
关于vue中如何监听数组变化
2021/04/28 Vue.js
详解Oracle数据库中自带的所有表结构(sql代码)
2021/11/20 Oracle