fiber架构
我们先简单了解一下
React的数据结构,通过jsx描述页面的数据结构,将jsx转化vdom,再转化成fiber。如上图是最简单的fiber树,父节点的child指向第一个子节点,子节点的sibling指向下一个兄弟节点,子节点上的return指向父节点。
多节点的diff
第一次遍历
遍历新节点的数组,复用key相同的老节点
let newIdx = 0;
let nextOldFiber = null;
for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {
nextOldFiber = oldFiber.sibling;
const newFiber = updateSlot(
returnFiber,
oldFiber,
newChildren[newIdx],
lanes,
);
if (newFiber === null) {
break;
}
oldFiber = nextOldFiber;
}
我们可以看到第一次遍历结束有两个条件
newChildren遍历完成- 新老节点的
key不相等,也就是newFiber为null的时候。
我们可以看下updateSlot函数,看下什么情况下返回null
function updateSlot(
returnFiber: Fiber,
oldFiber: Fiber | null,
newChild: any,
lanes: Lanes,
): Fiber | null {
const key = oldFiber !== null ? oldFiber.key : null;
if (typeof newChild === 'object' && newChild !== null) {
switch (newChild.$$typeof) {
case REACT_ELEMENT_TYPE: {
if (newChild.key === key) {
...
} else {
return null;
}
}
case REACT_PORTAL_TYPE: {
if (newChild.key === key) {
...
} else {
return null;
}
}
}
if (isArray(newChild) || getIteratorFn(newChild)) {
if (key !== null) {
return null;
}
}
}
return null;
}
当新老节点的key不相等的时候,结束第一次遍历
第二次遍历
如果旧节点遍历完成,将剩余新节点加上Placement副作用
if (oldFiber === null) {
for (; newIdx < newChildren.length; newIdx++) {
const newFiber = createChild(returnFiber, newChildren[newIdx], lanes);
...
}
}
第三次遍历
如果新旧节点都没有遍历完成,先将剩余的oldFiber节点生成一个map,遍历新节点,在旧节点的map中获取可以复用的oldFiber生成新的newFiber,将newFiber通过placeChild函数移动到对应的位置
// 初始化为0
let lastPlacedIndex = 0;
// 将剩余的oldFiber节点生成一个map
const existingChildren = mapRemainingChildren(returnFiber, oldFiber);
for (; newIdx < newChildren.length; newIdx++) {
// 从旧节点的map中获取是否有可以用的fiber
const newFiber = updateFromMap(
existingChildren,
returnFiber,
newIdx,
newChildren[newIdx],
lanes,
);
if (newFiber !== null) {
// 通过placeChild函数进行节点的移动
lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
...
}
}
function placeChild(
newFiber: Fiber,
lastPlacedIndex: number,
newIndex: number,
): number {
const current = newFiber.alternate;
const oldIndex = current.index;
if (oldIndex < lastPlacedIndex) {
// This is a move.
newFiber.flags = Placement;
return lastPlacedIndex;
} else {
// This item can stay in place.
return oldIndex;
}
}
我们主要关心如何进行移动的,着重分析一下placeChild函数
- 初始化lastPlacedIndex:
let lastPlacedIndex = 0 - 当
oldIndex < lastPlacedIndex时,进行节点的移动 - lastPlacedIndex的值是oldIndex和lastPlacedIndex两个中的最大值,即
lastPlacedIndex = oldIndex < lastPlacedIndex ? lastPlacedIndex : oldIndex
我们看下将ABCD,变成DABC的移动过程
- 初始化lastPlacedIndex为0
- 遍历newChildren, 找到D节点的oldIndex为3,
oldIndex < lastPlacedIndex不成立,所以D节点不移动,将lastPlacedIndex更新为3 - 找到A节点的ondIndex为0,
oldIndex < lastPlacedIndex成立,将A移动到最后面 - 找到B节点的ondIndex为1,
oldIndex < lastPlacedIndex成立,将B移动到最后面 - 找到C节点的ondIndex为2,
oldIndex < lastPlacedIndex成立,将C移动到最后面
为什么不用Vue2的双端diff
在处理D节点插入到A节点前面这种情况,React的diff算法移动次数过多,为什么不采取Vue2的双端diff?
官方给出的解释:
- 反转列表和后面的节点插入到前面的场景比较少
- 双端diff需要向前查找节点,但是fiber节点没有反向指针,只能从前向后遍历