React 渲染流程
首先我们需要了解React的整体流程:
- render阶段:计算要渲染的虚拟DOM (调度器Scheduler,协调器Reconciler)
- commit阶段:渲染UI(渲染器renderer)
如果再展开的细致一些:
- 调度器(Scheduler):调度任务,为任务排序优先级,让优先级更高的任务进入Reconciler;
- 协调器(Reconciler):生成Fiber对象, 收集副作用,找出哪些节点发生了变化,打上不同的flags(diff 算法就是发生在这个环节);
- 渲染器(Renderer):根据协调器计算出虚拟DOM渲染同步的渲染节点到视图上。
Diff 算法
谁和谁在 Diff?
前文我们提过,React的Fiber是双树共存状态:
- Current Fiber Tree (当前UI对应的Fiber Tree)
- WorkInProgress Fiber Tree (内存中构建的Fiber Tree)
除了这两个,我们还需要知道一个对象,就是我们写代码的JSX 被render调用之后返回的结果。也就是当前状态下描述的DOM信息。
DIff算法就是Current Fiber Tree 和 JSX 对象 进行对比,生成 WorkInProgress Tree。 生成完之后再进行替换。
生成新的Fiber Tree,如何提升效率呢?那当然是能复用就复用,不能复用的再重新生成。Diff算法的核心就是 复用。
React 对 Diff算法 进行的优化
Diff算法本身其实是十分消耗性能的。 两棵树的进行Diff算法时间复杂度为 O(n^3):
- 每对节点的比较需要遍历其所有子节点的组合。
- 对于两棵大小为
n
的树,可能的子树对数为O(n²)
。 - 每个子树对的比较需要遍历子节点组合,最坏情况下复杂度为
O(n)
。 - 总时间复杂度为 O(n²) × O(n) = O(n³) 。
所以React 对 Diff算法设置了限制,将复杂度降至O(n):
- 只对同级别的diff,
- 不同类型的元素也不会进行diff。 比如div变成了p。 会进行销毁再重建,
- 开发者通过key来暗示哪些子元素能够保持稳定。 比如两个元素,互换了位置, 如果没有key,就会进行销毁再重建, 如果使用了key,那么此时的DOM元素是可以复用的,直接互换位置。
key的作用
场景 1:无 Key 的列表
React 默认按索引顺序比对子节点。如果列表发生重排或中间插入元素,会导致大量节点被重新创建。
js
// 旧列表
<ul>
<li>A</li> {/* Index 0 */}
<li>B</li> {/* Index 1 */}
</ul>
// 新列表(在头部插入元素)
<ul>
<li>C</li> {/* Index 0 */}
<li>A</li> {/* Index 1 */}
<li>B</li> {/* Index 2 */}
</ul>
React 会发现索引 0 的节点从 A
变为 C
,索引 1 的节点从 B
变为 A
,最终销毁 A
和 B
,创建 C
、A
、B
,性能极差。
场景 2:有 Key 的列表
通过为子节点添加唯一的 key
,React 可以识别节点,从而避免不必要的销毁和重建。
less
// 旧列表
<ul>
<li key="a">A</li>
<li key="b">B</li>
</ul>
// 新列表(插入 C)
<ul>
<li key="c">C</li>
<li key="a">A</li>
<li key="b">B</li>
</ul>
React 会识别出 key="a"
和 key="b"
的节点只是位置变化,仅插入新节点 C
,避免重复渲染 A
和 B
。
单节点diff
单节点指的是新节点是单一节点, 但是旧节点是不一定。
此时判断diff是否能够复用的判断流程:
- 判断key是否相同, (更新前后如果不设置key, 也是相同的情况)
- 如果key相同,在判断type是否相同,如果type相同,就复用,如果type不同,无法复用(兄弟元素也不能复用,直接删除)。 如果key不同,直接不能复用(可以再遍历兄弟元素,是否可以复用)。
多节点diff
指新节点是多个,
React团队发现 对节点的更新操作要比 增删 移动 要多。 所以进行多节点diff的时候,React会进行两轮遍历
- 第一轮遍历会尝试逐个的复用节点。当遇到key不同的时候 无法复用就结束遍历。 如果遇到key相同, 但是type不同, 此时会将这个FiberNode添加到del数组中, 回头统一删除, 根据新的react元素创建一个新元素, 到那时遍历没有结束。 继续遍历 等到结尾, 或者等key 或者 type 不同 就会结束
- 第二轮遍历处理上一轮遍历中没有处理完的节点。 如果第一轮提前结束了, 说明没有JSX没有遍历完,或者CurrentFiberNode没有遍历完。 有三种情况, JSX遍历完了 到那时CurrentFiberNode没有遍历完,说明有需要被删除的节点。 第二种情况, CurrentFIber遍历完成了, 但是JSX没有遍历完 ,说明新增了,, 第三种情况下, 都有剩余, 将剩余的currentFibernode加入到map里面, 然后遍历JSX取再map中是否有能服用的, 找不到就新增。 遍历完之后, map中还有剩余 就将他们删掉。
总结
面试官问到diff算法的时候, 我们需要回答谁和谁Diff,diff的目的是什么,diff的核心是什么,key的作用是什么。