react diff算法完全解读
前言
diff算法在前端面试中也算是一个高频考题了,那怎么给面试官一个满分解答呢?难道还是简单的说个“深度优先,同层级比较”吗?这太短小精悍了……!
好了,下面开始进入正题
单节点diff
单节点diff就比较简单了,从同层级的老fiber
节点中找出key
值和type
都相等的老节点,如果该老fiber节点存在,则复用他,然后删除剩余的节点,否则重新生成一个新的fiber
节点(这也就意味着以这个节点为根的子树需要重新生成)。
下面我们来看看单节点diff
的核心源码
function reconcileSingleElement(
returnFiber: Fiber,
currentFirstChild: Fiber | null,
element: ReactElement,
lanes: Lanes
): Fiber {
const key = element.key;
let child = currentFirstChild;
// 遍历同层级的老fiber节点
while (child !== null) {
if (child.key === key) {
const elementType = element.type;
// 从老fiber节点中找出key和type都相同的节点,如果找到则将该节点复用,并删除多余的节点,退出循环
if (child.elementType === elementType) {
deleteRemainingChildren(returnFiber, child.sibling);
const existing = useFiber(child, element.props);
existing.ref = coerceRef(returnFiber, child, element);
existing.return = returnFiber;
return existing;
}
deleteRemainingChildren(returnFiber, child);
break;
} else {
// 如果该fiber节点没匹配上,则删除
deleteChild(returnFiber, child);
}
child = child.sibling;
}
// 能走到这里就意味着无法从老fiber中匹配到key和type都相同的节点,无法复用,需要重新生成
const created = createFiberFromElement(element, returnFiber.mode, lanes);
created.return = returnFiber;
return created;
}
复制代码
多节点diff
重点来了,注意了啊,多节点才是精髓
目前页面上有3个li,内容和key分别为1、2、3(fiber
树如下图所示)
现在我要让页面变成3个li,内容和key分别是1、3、2,其所对应的ReactElement
结构为
[
{$$typeof: REACT_ELEMENT_TYPE, type: 'li', props: {children: 1}, key: 1, ...},
{$$typeof: REACT_ELEMENT_TYPE, type: 'li', props: {children: 3}, key: 3, ...},
{$$typeof: REACT_ELEMENT_TYPE, type: 'li', props: {children: 2}, key: 2, ...},
]
复制代码
那么react怎么做呢?
- 首先会定义一个
lastPlacedIndex
用来作为基准位置来判断旧节点是否需要移动,初始为0 - 第一轮遍历新的
children
即ReactElement
,即1、3、2,遍历的同时,旧fiber也跟着指向下一个兄弟节点,如果key和type都相同,则代表该fiber节点可以复用,否则退出第一轮循环。看到这,该oldFiber是可以复用,将该oldFiber的index值与lastPlaceIndex比较,如果oldIndex<lastPlaceIndex,则表示新生成的fiber节点需要移动,打上Placement的标记,lastPlaceIndex保持不变,否则lastPlaceIndex=oldIndex
- 第一轮循环结束后,有三种情况:1.newChildren都遍历完(最理想的情况,只需要删除剩余的oldFibers);2.oldFibers遍历完了,但是newChildren还没遍历完(这种情况也没那么复杂,只需要为剩余的newChildren创建fiber节点即可);3.newChildren和oldFibers都没遍历完(最复杂的情况了,需要开启第二轮循环)
- 将剩余的oldFibers存入map中,oldFiber有key的以key作为map的key值,没key的以index作为map的key值,oldFiber作为value值
- 第二轮循环开启,遍历未遍历完的newChildren:从map中寻找以
newChild.key||newIndex
为key的oldFiber,如果该oldFiber存在,则使用它(type相同则复用,不相同则生成)创建newFiber
,然后将该oldFiber从map中删除,然后将该如果该newFiber
是复用来的,他就存在alternate
,newFiber.alternate == oldFiber
,可以跟lastPlaceIndex进行比较如果oldIndex<lastPlaceIndex,则表示新生成的fiber节点需要重新插入,打上Placement的标记,lastPlaceIndex保持不变,否则lastPlaceIndex=oldIndex
;如果newFiber
是重新生成的,则newFiber.alternate == null
,直接打上Placement
标签即可,lastPlaceIndex保持不变。 - 第二轮循环结束后,删除map中剩余的oldFiber
复杂的流程如下图所示:
下面来看下react diff代码片段的实现
function reconcileChildrenArray(
returnFiber: Fiber,
currentFirstChild: Fiber | null,
newChildren: Array<*>,
lanes: Lanes
): Fiber | null {
let resultingFirstChild: Fiber | null = null;
let previousNewFiber: Fiber | null = null;
let oldFiber = currentFirstChild;
let lastPlacedIndex = 0;
let newIdx = 0;
let nextOldFiber = null;
// 第一轮遍历
for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {
// ...
nextOldFiber = oldFiber.sibling;
// 如果type和key相同则复用该fiber节点返回一个newFiber,否则返回null,然后跳出第一轮循环
const newFiber = updateSlot(
returnFiber,
oldFiber,
newChildren[newIdx],
lanes
);
if (newFiber === null) {
// ...
break;
}
if (oldFiber && newFiber.alternate === null) {
// 兜底操作,如果该newFiber不是复用来的,就将oldFiber标记为删除
deleteChild(returnFiber, oldFiber);
}
// 标记该newFiber是否需要重新插入
lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
if (previousNewFiber === null) {
resultingFirstChild = newFiber;
} else {
previousNewFiber.sibling = newFiber;
}
previousNewFiber = newFiber;
oldFiber = nextOldFiber;
}
// 第一轮循环结束,newChildren遍历完成,删除多余的oldFiber
if (newIdx === newChildren.length) {
// We've reached the end of the new children. We can delete the rest.
deleteRemainingChildren(returnFiber, oldFiber);
return resultingFirstChild;
}
if (oldFiber === null) {
// 老fiber被遍历完了,但是newChildren还未遍历完,则需要生成fiber并标记为需要插入
for (; newIdx < newChildren.length; newIdx++) {
const newFiber = createChild(returnFiber, newChildren[newIdx], lanes);
if (newFiber === null) {
continue;
}
lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
if (previousNewFiber === null) {
resultingFirstChild = newFiber;
} else {
previousNewFiber.sibling = newFiber;
}
previousNewFiber = newFiber;
}
return resultingFirstChild;
}
// 将剩余的oldFiber存入map中,key=oldFiber.key||oldFiber.index
const existingChildren = mapRemainingChildren(returnFiber, oldFiber);
// 遍历剩余的newChildren
for (; newIdx < newChildren.length; newIdx++) {
// 从map中查询是否有oldFiber可以复用,根据newChild.key||newIndex来查询,有则复用,没有则重新生成
const newFiber = updateFromMap(
existingChildren,
returnFiber,
newIdx,
newChildren[newIdx],
lanes
);
if (newFiber !== null) {
// 走到这里说明newFiber是生成而来的
if (newFiber.alternate !== null) {
// 从map中移除已经被复用的oldFiber
existingChildren.delete(newFiber.key === null ? newIdx : newFiber.key);
}
// 判断该节点是否需要移动
lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
if (previousNewFiber === null) {
resultingFirstChild = newFiber;
} else {
previousNewFiber.sibling = newFiber;
}
previousNewFiber = newFiber;
}
}
// 删除map中剩余的oldFiber
existingChildren.forEach((child) => deleteChild(returnFiber, child));
return resultingFirstChild;
}
复制代码
留个思考题?如果diff过程中,oldFibers中有部分节点的key值相同,会造成什么问题呢?
© 版权声明
文章版权归作者所有,未经允许请勿转载。
THE END