了解过react的都必定会知道 virtual DOM 的存在,不夸张的说,virtual DOM 就是 react 最核心的技术。virtual DOM 就如一个征战南北的猛将,为王打下了如今前端领域的半片江山。如果你想了解 react 王朝,那么必须先了解 virtual DOM,了解这个王朝几乎所有的生命力和战斗力所在。
有人会觉得我夸张了,认为即使不懂 virtual DOM,也照样可以用react来开发应用。是的没错,但这并不能否认 virtual DOM 的重要性,事实上,你使用 react 的时候,之所以能够得心应手地开发着大型应用,而不用瞻前顾后地考虑着性能问题,功劳依然来自于 virtual DOM。
virtual DOM
性能
大多数人对 virtual DOM 的认知,不外乎:
- 在浏览器内存中维护着的一棵与页面 DOM 结构一致的对象树
- 依靠 diff 算法极大提高了 DOM 操作的性能
但并不知道 virtual DOM 是如何提高性能的。我们暂且抛开这个问题,先来了解一下浏览器是怎么将DOM反映到页面上的。
浏览器工作流
创建 DOM 树
一旦浏览器接收到一个 HTML 文件,渲染引擎(render engine)就开始解析它,并根据 HTML 元素(elements)一一对应地生成 DOM 节点(nodes),组成一棵 DOM 树。
创建渲染树
同时,浏览器也会解析来自外部 CSS 文件和元素上的 inline 样式。在这个过程中,浏览器会逐步对各个节点计算最终样式,并为包含样式信息的 DOM 树上的节点,再创建另外一个树,一般被称作渲染树(render tree)。
布局
构造了渲染树以后,浏览器引擎开始着手布局(layout)。布局时,渲染树上的每个节点根据其在屏幕上应该出现的精确位置,分配一组屏幕坐标值。
绘制
接着,浏览器将会通过遍历渲染树,调用每个节点的 paint 方法来绘制节点在渲染树创建阶段返回的 render 对象。通过绘制,最终将在屏幕上展示内容。
性能瓶颈
从上边浏览器的工作流可以看出,每一次的 DOM 操作,都会引发一次从创建 DOM 树、创建渲染树、布局到绘制的全过程,尤其是在创建渲染树阶段,对节点样式的计算量通常很大。而正常的,由用户引发的页面改变往往不止一次的 DOM 操作,多次计算,将导致页面性能大幅降低。
virtual DOM 做了什么
通过分析,我们可以很清楚的意识到,多次的 DOM 操作引发的多次计算,是导致页面性能低的主要原因。而 virtual DOM 的解决方法很简单,批量处理 DOM 操作。virtual DOM 实际上是起了一个缓冲的作用,它将一个事件循环(event loop)中发出的 DOM 操作全部收集起来,不立即在页面上产生效果,而是在事件循环的结尾,才向页面作用,从而合并多次的 DOM 操作为一次计算。
在此基础上,virtual DOM 通过 Diff 算法,以优化的策略计算出最小的差别,并作用到真实的DOM上。
通过合并 DOM 操作和diff算法,virtual DOM 有效地解决了 DOM 操作所带来的性能问题,使得 react 在开发大型复杂的单页面应用中脱颖而出,大放异彩。
独特的Diff算法
为什么还需要 Diff 算法呢?这是因为在 web 页面中,DOM 树结构通常比较稳定,对于其中某个或某几个 DOM 节点的修改,没必要重新创建一颗 DOM 树。通过 Diff 算法,react将一次计算中多余的渲染工作尽最大化去除,从而进一步提升了页面性能。
实际上,Diff 算法不是react首创,但却是在 react 这里得到了突破性的优化。传统标准的 Diff 算法复杂度达到了 O(n^3),这就意味着,如果要展示1000个节点,就要依次执行上十亿次的比较。这是绝对无法满足性能需求的。而 react 开发团队通过制定大胆的策略,使得 Diff 算法复杂度降到 O(n)。
策略之所以大胆,是因为算法有所冒险。react的Diff算法是基于以下三个现实策略进行优化的:
1、Web UI 中 DOM 节点跨层级的移动操作特别少,可以忽略不计;
2、拥有相同类的两个组件将会生成相似的树形结构,拥有不同类的两个组件将会生成不同的树形结构;
3、对于同一层级的一组子节点,它们可以通过唯一 id 进行区分。
基于以上三个前提策略,React 分别对 tree diff、component diff 以及 element diff 进行算法优化,事实也证明这三个前提策略是合理且准确的,它保证了整体界面构建的性能。
tree diff
基于策略一,React 对树的算法进行了简洁明了的优化,即对树进行分层比较,两棵树只会对同一层次的节点进行比较。
既然 DOM 节点跨层级的移动操作少到可以忽略不计,针对这一现象,React 通过对 Virtual DOM 树进行层级控制,只会对同一个父节点下的所有子节点进行比较。当发现节点已经不存在,则该节点及其子节点会被完全删除掉,不会用于进一步的比较。这样只需要对树进行一次遍历,便能完成整个 DOM 树的比较。
当然,这个策略的风险性就在于,当发生 DOM 节点跨层级的移动操作时,react的处理方式将极其残暴,他会先创建新的节点,再删除原来需要移动的节点。因此,react 官方也建议不要进行 DOM 节点跨层级的操作。
component diff
React 是基于组件构建应用的,对于组件间的比较所采取的策略也是简洁高效。
- 如果是同一类型的组件,按照原策略继续比较 virtual DOM tree。
- 如果不是,则将该组件判断为 dirty component,从而替换整个组件下的所有子节点。
- 对于同一类型的组件,有可能其 Virtual DOM 没有任何变化,如果能够确切的知道这点那可以节省大量的 diff 运算时间,因此 React 允许用户通过 shouldComponentUpdate() 来判断该组件是否需要进行 diff,而这个 API 也成为了 react 性能优化的常见手段。
element diff
当节点处于同一层级时,React diff 提供了三种节点操作,分别为:INSERT_MARKUP(插入)、MOVE_EXISTING(移动)和 REMOVE_NODE(删除)。
- INSERT_MARKUP,新的 component 类型不在老集合里, 即是全新的节点,需要对新节点执行插入操作。
- MOVE_EXISTING,在老集合有新 component 类型,且 element 是可更新的类型,generateComponentChildren 已调用 receiveComponent,这种情况下 prevChild=nextChild,就需要做移动操作,可以复用以前的 DOM 节点。
- REMOVE_NODE,老 component 类型,在新集合里也有,但对应的 element 不同则不能直接复用和更新,需要执行删除操作,或者老 component 不在新集合里的,也需要执行删除操作。
值得注意的是,react的性能优化并不是什么神秘的事,任何项目都可以运用类似方法去改善页面性能,只不过,react帮你做了这些繁琐的工作。
抽象
到此,我们了解了 virtual DOM 对性能的优化方案,也足以意识到高性能作为 react 的王牌优势,virtual DOM 在其中所扮演的重要角色。但是如果谈到 virtual DOM,你只想起 “提升性能” 这一个关键词的话,那就说明你对 virtual DOM 还不够了解,事实上,virtual DOM 最创造性最颠覆式的意义,在于抽象。
我们知道,virtual DOM 对真实 DOM 进行了一层抽象,它帮助我们去操作真实 DOM,而我们通过操作 virtual DOM 来控制页面 UI。在使用 react 之前,我们的 js 代码和 UI 是完全耦合的。但是 virtual DOM 强制在逻辑代码和 UI 成分之间构建了一层隔离,使得逻辑和视图低耦化,极大提高了代码复用性。于是我们发现,一套逻辑,可以对应多个 UI。这对于代码移植和维护是具有重大意义的。react native 的推出,完全说明了 virtual DOM 的颠覆性意义。
上图中,virtual DOM可以映射到web端、IOS端、安卓平台,在不同环境下的不同表现,均使用了同一套业务逻辑。
这才是 virtual DOM 的灵魂所在,正如 react native 的理念——“Learn Once ,Write Anywhere”。 其实在 Vue 、Angular 2相继推出后,react 的性能优势已慢慢不再明显,但是 virtual DOM 的革命性意义,依然保持着 react 在前端领域中不可撼动的地位,保 react 王朝生生不息。
末尾
在写这篇博文之前,我已经在许多项目中反复地使用过 react 了,但我最近在思考,我到底了解不了解它。当然,答案的确认花不了三秒:不。甚至是一概不知。心血来潮地翻阅了很多疑惑之处,自觉醍醐灌顶,也分享给正在路上的各位。
参考资料
http://www.infoq.com/cn/articles/subversion-front-end-ui-development-framework-react/
http://blog.csdn.net/lihongxun945/article/details/46640503
http://blog.csdn.net/yczz/article/details/49886061
http://www.tuicool.com/articles/Ar6Zruq
http://www.cnblogs.com/mooniitt/p/6064749.html