React最神奇的亮点就是虚拟DOM和高效的diff算法。
在React中,UI界面由组件构成。当前组件的状态发生变化时,真实DOM树需要重新更新渲染,而真实DOM树来源于React的虚拟DOM树。React将虚拟DOM树转换为真实DOM树的最小计算过程称为调和(reconciliation),而diff算法便是调和的具体实现!
setState()
方法会更改组件状态,真实DOM树需要重新更新渲染- 调用
setState()
方法,React不是立即对其更新,而是使用了事件轮询对变更做批量处理绘制。(多个状态改变合并到一次页面更新中) - 真实DOM和虚拟DOM是通过ReactDOM转换器适配转换
React diff 算法 |
简单来说,diff算法就是给定任意的两棵树从中找到最少的转换步骤,或者说是从上一个渲染转到下一个渲染的最少步骤。
diff 策略 |
diff 算法并非React 推出的,我们管它叫传统diff算法,React中的diff算法则是对传统diff算法的做了极大性能提升。
diff算法作用 |
计算出虚拟DOM中真正变化的部分,并只针对该部分进行真实DOM操作,而非重新渲染整个页面。不是重新丢一份源代码给浏览器重新加载渲染到页面,而是告诉浏览器你只要修改哪一部分即可。
传统diff算法 |
传统diff算法是将新旧两棵树每个节点逐一对比,循环遍历所有子节点,然后判断子节点的更新状态(又一次遍历),其复杂度为O(n^3)(n是树中节点的总数),效率很低。有多低呢?打个比方,假如有1 000个元素,那么需要计算10亿次左右。将此算法应用到计算机用于前端渲染,那么代价太大了。而React将时间复杂度为O(n^3)的算法直接转为O(n),具体怎么实现的呢?
React diff策略 |
diff 的核心:对比和修改。 React基于这两点实现了一个启发式的O(n)的算法:
- 两个不同类型的元素将产生两个不同的树;
- 同一级的一组子节点,可以从中埋入一个
key
属性用于区分;
在此基础上React大胆采用了3种策略:
- DOM节点跨层级操作特别少,所以可以忽略;
- 拥有相同类的两个组件会生成相似树形结构,拥有不同类的两个组件将会产生不同树形结构;
- 一层级一组子节点通过唯一id(
key
)区分。
下面来具体介绍这3种策略的具体做法。
Tree Diff |
Tree Diff是两棵新旧虚拟DOM树按照层级的对应关系,把同一层级的节点遍历一遍,即同层比较,这样就能快速找到有差异的地方。
同层求异 |
它首先会比较最顶层的虚拟DOM节点是否一致,如果一致的话,就继续比较下一层的节点;如果不一致的话,react就会把这个节点及其下面所有节点全部删掉,重新生成一遍新的DOM,然后用新的DOM替换原始页面的DOM。也就是只需要遍历一遍。
Tree的比较适用于界面DOM节点跨层级操作少的情形,这样就可以忽略不计层级带来的影响。
DOM跨层级操作 |
如果DOM节点出现了跨层级操作,diff 会咋办呢?
答:diff只简单考虑同层级的节点位置变换,如果是跨层级的话,只有创建节点和删除节点的操作。
如上图所示,以A为根节点的整棵树会被重新创建,而不是移动,因此 官方建议不要进行DOM节点跨层级操作,可以通过CSS隐藏、显示节点,而不是真正地移除、添加DOM节点。
Component Diff |
React对组件的比较,有两种策略:
- 对于类型相同的组件,根据Virtual DOM树按照原来的策略(Tree->Component->Element )继续比较Virtual DOM Tree即可。
- 对于类型不同的组件,React会将这个组件内部所有的子节点重新替换。
组件B变为组件C,虽然子组件内容相似,但一旦识别到父组件B和C不同,就直接删除B组件,重新创建C组件,B组件在React中称为dirty component
(可以理解为组件最近一次发生改变,但还未重新渲染的组件)
Element Diff |
React在遇到类型相同的组件时(衔接Component Diff类型相同情况),会继续对组件内部元素进行对比,检查内部元素异同,这就是Element Diff。(节点处于同一层级时)它可以进行插入、移动、删除这3种操作。
- 插入:元素 C 不在集合
(A,B)
中,需要插入 - 删除:
- 元素 D 在集合
(A,B,D)
中,但 D的节点已经被完全更改,不能复用和更新,所以需要删除旧的 D ,再创建新的。 - 元素 D 之前在 集合
(A,B,D)
中,但集合变成新的集合(A,B)
了,D 就需要被删除。
- 元素 D 在集合
- 移动:元素 D已经在集合
(A,B,C,D)
里了,且集合更新时,D没有发生更新,只是位置改变,如新集合(A,D,B,C)
,D在第二个,无须像传统diff,让旧集合的第二个B和新集合的第二个D 比较,并且删除第二个位置的B,再在第二个位置插入D,而是 (对同一层级的同组子节点) 添加唯一key
进行区分,移动即可。
传统 diff 移动同级元素 |
旧元素A、B、C、D发生变化需要排列为B、A、D、C,当发现B不等于A时,则将B节点创建并插入至新组建,同时删除A节点,以此类推。即使有相同的节点,而且仅仅只是移动了位置,但还是需要删除并重写,无疑这种操作很繁琐低效。
React diff 移动同级元素 |
在React中,它可以给每个同层节点设置一个唯一的key。当元素A、B、C、D发生变化需要排列为B、A、D、C时,diff差异化对比后发现新旧节点存在相同的,则无须进行重新创建和删除,只需将旧的节点集合进行位置移动即可。
其内部具体执行是,先将新的节点集合进行遍历循环,然后通过唯一标记key去老的节点集合中寻找是否有命中的标记,如果有,就执行移动操作。与此同时需要注意的是,(执行移动操作的节点)当前节点在旧集合中索引的值oldIndex必须小于新集合中当前节点的索引值newIndex,因为这样能节省更多不必要的操作从而节省时间,更加优化算法效率。
总结 |
- Tree Diff:采用分层求异的策略,将新旧两棵DOM树按照层级对应的关系进行对比,这样只需要对树进行一次遍历,就能够找到哪些元素是需要更新的。
- Component Diff:查看两个组件的类型是否相同。如果类型不同,则需要更新,更新时先把旧的组件删除,再创建一个新的组件插入之前删除的位置。类型相同时,暂时不需要更新。
- Element Diff:通过设置唯一key值,对元素diff进行优化。(组件类型相同时看内部元素)元素发生了改变,则找到需要修改的元素,有针对性进行修改。
=============================================================================================
习惯边学习边笔记以加深记忆,本文主要参阅《React+Redux前端开发实战》一书相关章节,加以自己理解作补充而行文。