React 为什么要使用虚拟 DOM,什么是虚拟 DOM ?

React 已经问世一段长时间了,现在也是几个热门框架之一,每个框架多少都会自己定义一些概念,比如 Angular 有控制器,指令,过滤器…. 而 React 定义了单向数据绑定,组件和虚拟 DOM,前两个很好理解,唯独只有虚拟 DOM ,常让初学者一头雾水,或者两头,虽然网上有很多相关的文章,我决定再补一篇。

为什么要搞出一个虚拟 DOM,最直接的原因就是,直接操作 DOM,效率不高,出于历史原因,导致 DOM 是一个很庞大的东西。为了搞明白虚拟 DOM,有必要先来复习一下浏览器的渲染流程,以及什么是回流和重绘,请看图:

浏览器的渲染流程

这张图是 Webkit 内核的渲染流程,其它内核的可能略差别,但都大同小异吧。先一步一步做个分析。

当浏览器接收到 HTML 文件的时候,渲染引擎会解析它并创建一个节点的 DOM 树,于 HTML 元素是一对一的关系。

同时,从外链的 CSS 文件也会被接收进来,然后根据样式描写,被解析,沿着 DOM 树形成一个对应的渲染树。

拿 WebKit 内核来讲,DOM 树里的所有节点都有一个 “attach(附加)“的方法,样式规则被当作 “attachment(附件)”,添加进去。该方法会计算样式信息,然后返回一个渲染对象,也称作渲染器。“附件”是同步的,当节点被插入 DOM 树的时候,会回调“附加”这个方法。

渲染树完成之后,会经过一个布局(layout)的过程,每个节点都会根据自己的宽高被赋予相对于屏幕的坐标位置。

下一步就是样式的渲染了,渲染树遍历每个节点的 “paint()” 方法(浏览器后台的 API ),最后生成内容出现在屏幕上。

看了以上的渲染流程,每次 DOM 的结果发生变化的时候,都会整个流程重走一遍,也就是回流和重绘。

回流 Reflow。当它发现某个部分发生了变化影响了布局,需要倒回去重新渲染,我们就称这个回退的过程叫 Reflow。只要某些行为引起了页面上某些元素的占位面积、定位方式、边距等属性的变化,都会引起这些元素内部、周围甚至整个页面的重新渲染。

重绘 Repaint。比较好理解,其实就是浏览器根据重新计算的各个属性值对页面的部分元素进行重新绘制。如果只是改变某个元素的背景色、文字颜色、边框颜色等等不影响它周围或内部布局的属性,将只会引起浏览器 Repaint。Repaint 的速度明显快于 Reflow。

如果是一个复杂的单页面应用,往往会涉及到大量的 DOM 操作,这意味着多个计算步骤在重复,导致性能的降低。

在这个过程中,虚拟 DOM 真正的作用是,当你试图在改变 DOM 的时候,React 并不会直接去操作 DOM,而是先经过虚拟 DOM,进行比较计算,然后 React 再去操作真正的 DOM,这样做的目的只有一个,把布局回流重回的次数降到最低。

这样可能有点不太具体,此处应该讲个例子:

比如一个函数里,执行下来,一次性要修改 30个节点,这意味着要进行 30次的回流与 30 次的重绘。如果是经过虚拟 DOM 的话(虚拟 DOM 存在于 JavaScript 内存中),虚拟 DOM 会先在内存中进行这 30次修改的计算,得出最终结果,然后在真正去操作 DOM,也就是可以把回流与重绘降低到了一次。

如果没有使用虚拟 DOM,自己也可以优化这一点,使用 DOM fragment 先进行操作。