在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):
可以看到,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 快: (注意 angular 默认版本无优化,优化过的在下面)
在比较性能的时候,要分清楚初始渲染、小量数据更新、大量数据更新这些不同的场合。virtual dom、脏检查 mvvm、数据收集 mvvm 在不同场合各有不同的表现和不同的优化需求。virtual dom 为了提升小量数据更新时的性能,也需要针对性的优化,比如 shouldcomponentupdate 或是 immutable data。
四、对react虚拟dom的误解?
react 从来没有说过 “react 比原生操作 dom 快”。react给我们的保证是,在不需要手动优化的情况下,它依然可以给我们提供过得去的性能。
react掩盖了底层的 dom 操作,可以用更声明式的方式来描述我们目的,从而让代码更容易维护。下面还是借鉴了知乎上的回答:没有任何框架可以比纯手动的优化 dom 操作更快,因为框架的 dom 操作层需要应对任何上层 api 可能产生的操作,它的实现必须是普适的。针对任何一个 benchmark,我都可以写出比任何框架更快的手动优化,但是那有什么意义呢?在构建一个实际应用的时候,你难道为每一个地方都去做手动优化吗?出于可维护性的考虑,这显然不可能。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持移动技术网。
如对本文有疑问, 点击进行留言回复!!
offset、client、scroll (width,height、left,top、X,Y)
网友评论