前言
在 React 中,Diff 算法是实现高性能 UI 更新的功臣。它将传统树 Diff 的 复杂度直接优化到了 。而在react16 Fiber 架构引入后,这一过程变得更加精妙——它不再是简单的树对比,而是新虚拟 DOM 数组与旧 Fiber 链表之间的“博弈”。
一、 核心设计思想:三条“军规”
React Diff 算法基于三个预设前提,这也是其高效的原因:
- 同层级对比 (Level Diff) :只比较同一层级的节点,不跨层比较。如果一个节点位置变了,React 会销毁它并在新位置重建,而不会去层级中寻找。
- 节点类型判断 (Type Diff) :类型不同(如
div换成span),直接视为新树,销毁旧的及其所有子节点。 - 稳定 Key 标识 (Key Diff) :通过唯一的
key识别节点的稳定性,实现节点复用。
二、 算法实现流程:单节点与多节点
React 会根据新生成的 ReactElement 子节点数量,分为两种 Diff 模式:
1. 单节点 Diff (Single Node)
单节点diff是指新的虚拟dom树父节点下只有一个子元素,而旧的dom树同一层级的dom节点下可能有1个或者多个子元素的情况。当新节点只有一个时,逻辑相对简单。React 会在旧 Fiber 链表中寻找 key 和节点类型 type 都匹配的节点。
- 复用成功:
key和type均相同。直接复用 Fiber,并将旧节点的兄弟节点全部标记删除。 - 彻底重建:
key相同但type不同,说明结构已变,删除旧节点及其兄弟,创建新节点。 - 继续寻找:
key不同,直接创建一个新节点,并将同一层级所有旧节点均删除。
2. 多节点 Diff (Multiple Nodes)
多节点 diff 是核心难点,多节点diff主要通过一轮或者两轮遍历,尽可能多的复用key和type都相同的节点。
第一轮遍历:从头开始遍历新节点子组件和旧的Fiber链表
主要寻找位置未变的节点。
- 按索引同时遍历新数组和旧 Fiber 链表。
- 如果
key和type匹配,则复用并继续。 - 一旦发现
key或者type不匹配,则表明节点不能复用,直接中断第一遍遍历,进入到第二轮遍历。
第二轮遍历:核心是通过移动、删除、新增来处理节点非连续性的复用
处理第一轮剩下的“乱序”节点。
-
构建映射表:首先根据剩余旧的fiber链表,构建一个
Map,其中key为键,旧fiber节点为值,接着设置一个lastPlacedIndex表示最后一个被复用且不需要移动的节点在旧fiber链表中的索引位置,初始值为0。 -
遍历数组 :接着遍历新节点数组,对与当前节点首先判断它的key是否存在于构建的Map结构中
- 不存在:表示无复用节点,直接新建节点
- 存在:首先找到当前节点在旧fiber链表中对应的索引
oldIndex,然后比较oldIndex与LastPlacedIndex,根据比较结果会有两种情况:- 如果
oldIndex >= lastPlacedIndex,说明该节点在旧集合中的位置相对靠后,不需要移动 - 如果
oldIndex < lastPlacedIndex,说明该节点在旧集合中位于当前参考节点之前,但现在排到了后面,因此需要被移动(标记为Placement) - 无论是否需要移动,都会将
lastPlacedIndex更新为oldIndex与lastPlacedIndex中的较大值Math.max(oldIndex, lastPlacedIndex) - 遍历完旧fiber链表后,如果发现还存在多余节点则直接删除
- 如果
三、 案例推演
旧fiber链表:a->b->c->d->e,新节点数组a->b->e->c->x->y
第一轮遍历:发现a,b节点可复用,同一索引下c、e不同,这时遍历中断,进入第二轮遍历
第二轮遍历先根据剩余的旧fiber链表构建map结构
existingFiberMap = {
'c': fiberC, // oldIndex=2
'd': fiberD, // oldIndex=3
'e': fiberE // oldIndex=4
}
- 设置
lastPlacedIndex =0,发现节点e存在于map结构中,这时e在旧链表中的索引为4,且oldIndex>lastPlacedIndex,说明节点可以复用且不需要移动,这时将lastPlacedIndex=Math.max(oldIndex, lastPlacedIndex)= 4,并将e从map中删除
继续遍历下一个节点c,这时lastPlacedIndex为4,map结构如下:
existingFiberMap = {
'c': fiberC, // oldIndex=2
'd': fiberD, // oldIndex=3
}
- 这时遍历到节点c,现节点c存在于map结构中,这时c在旧链表中的索引为2,
oldIndex < lastPlacedIndex,说明节点可以复用但是需要移动,再将lastPlacedIndex=Math.max(oldIndex, lastPlacedIndex)= 4,并将c从map中删除
继续遍历下一个节点x,这时lastPlacedIndex为4,map结构如下:
existingFiberMap = {
'd': fiberD, // oldIndex=3
}
遍历到下一节点x,发现x不存在于map结构中,说明没有可复用节点,这时需要新增一个x节点。
继续遍历下一个节点y,这时lastPlacedIndex为4,map结构如下:
existingFiberMap = {
'd': fiberD, // oldIndex=3
}
遍历到下一节点y,发现y不存在于map结构中,说明没有可复用节点,这时需要新增一个y节点。
遍历完成,发现map结构中还存在d节点,直接全部删除。
最终结果:
a,b,e节点可直接复用,且无需移动c节点可以直接复用,但是需要移动x,y节点需要新建并插入到c节点之后d节点需要删除
四、 总结
- Diff 是浅层的:React 依赖稳定的
key。如果你在更新时改变了key,React 会认为是全新的节点,从而触发不必要的重新挂载。 - 不要跨层级移动 DOM:因为同层对比机制,跨层级移动会导致“销毁 A 层 -> 在 B 层创建”,性能代价较高。