三种情况
节点的位置没有变化
// 更新前
<ul>
<li key="1" className="before">1</li>
<li key="2">2</li>
</ul>
// 更新后
<ul>
<li key="1" className="after">1</li>
<li key="2">2</li>
</ul>
节点的增加或删除
// 更新前
<ul>
<li key="1">1</li>
<li key="2">2</li>
<li key="3">3</li>
</ul>
// 删除
<ul>
<li key="1">1</li>
<li key="2">2</li>
</ul>
//增加
<ul>
<li key="1">1</li>
<li key="2">2</li>
<li key="3">3</li>
<li key="4">4</li>
</ul>
节点的移动
// 更新前
<ul>
<li key="1">1</li>
<li key="2">2</li>
<li key="3">3</li>
</ul>
// 移动
<ul>
<li key="1">1</li>
<li key="3">3</li>
<li key="2">2</li>
</ul>
实现思路:
-
两轮遍历,
-
第一轮遍历尝试复用所有节点
此情况最为常见,节点的位置没有变化(作为性能优化)
-
第一轮遍历后有一下三种情况
-
新节点遍历完毕, 删除剩余的老节点
-
老节点遍历完毕, 新增剩余的新节点
-
新老节点有剩余, 处理节点的移动
-
处理节点的移动 情况
- 将所有未处理的
oldFiber存入 带Map的结构中
function mapRemainingChildren( currentFirstChild: Fiber ): Map<string | number, Fiber> { // Add the remaining children to a temporary map so that we can find them by // keys quickly. Implicit (null) keys get added to this set with their index // instead. const existingChildren: Map<string | number, Fiber> = new Map(); let existingChild: null | Fiber = currentFirstChild; while (existingChild !== null) { if (existingChild.key !== null) { existingChildren.set(existingChild.key, existingChild); } else { existingChildren.set(existingChild.index, existingChild); } existingChild = existingChild.sibling; } return existingChildren; }- 第二轮遍历 剩下
newChildren- 如何判断节点的移动 判断的方法是 lastPlaceIndex 变量
function placeChild( newFiber: Fiber, lastPlacedIndex: number, newIndex: number ): number { newFiber.index = newIndex; if (!shouldTrackSideEffects) { // During hydration, the useId algorithm needs to know which fibers are // part of a list of children (arrays, iterators). newFiber.flags |= Forked; return lastPlacedIndex; } const current = newFiber.alternate; if (current !== null) { 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; } } else { // This is an insertion. newFiber.flags |= Placement; return lastPlacedIndex; } }
-
举例
// 更新前
<ul>
<li key="1">1</li>
<li key="2">2</li>
<li key="3">3</li>
</ul>
// 移动
<ul>
<li key="1">1</li>
<li key="3">3</li>
<li key="2">2</li>
</ul>
第一轮遍历开始
-
1(前) 与 1(后),可以复用, oldFiber.index = 0, lastPlaceIndex = 0
-
3(后) 与 (2) 前 ,不可复用, 跳出第一轮循环, lastPlaceIndex = 0 此时是 oldFiber 与 newChildren 都有剩余 将老节点 存放在Map中 以
key或fiber.index做索引 以fiber做value,结构如下:{ "2": FiberNode, "3": FiberNode, }
开启第二轮遍历: 遍历剩余newChildren
- "3" 在map中找到,可以复用 oldFiber.index = 2, 因为 oldFiber.index > lastPlaceIndex 所以 3 的位置不变,则lastPlaceIndex = 2
- "2" 在map中找到,可以复用 oldFiber.index = 1, 因为 oldFiber.index < lastPlaceIndex 所以 2 的位置变化,则lastPlaceIndex = 2
第二轮遍历结束
- 最后 2 背标记移动 由于 2 对应的fiber 没有 sibling 在 commitPlacement 方法中不存在 befoe 所以执行
parentNode.appendChild方法,对应的dom元素被添加父节点的最后
function commitPlacement(finishedWork: Fiber): void {
if (!supportsMutation) {
return;
}
if (supportsSingletons) {
if (finishedWork.tag === HostSingleton) {
// Singletons are already in the Host and don't need to be placed
// Since they operate somewhat like Portals though their children will
// have Placement and will get placed inside them
return;
}
}
// Recursively insert all host nodes into the parent.
const parentFiber = getHostParentFiber(finishedWork);
switch (parentFiber.tag) {
case HostSingleton: {
if (supportsSingletons) {
const parent: Instance = parentFiber.stateNode;
const before = getHostSibling(finishedWork);
// We only have the top Fiber that was inserted but we need to recurse down its
// children to find all the terminal nodes.
insertOrAppendPlacementNode(finishedWork, before, parent);
break;
}
// Fall through
}
case HostComponent: {
const parent: Instance = parentFiber.stateNode;
if (parentFiber.flags & ContentReset) {
// Reset the text content of the parent before doing any insertions
resetTextContent(parent);
// Clear ContentReset from the effect tag
parentFiber.flags &= ~ContentReset;
}
const before = getHostSibling(finishedWork);
// We only have the top Fiber that was inserted but we need to recurse down its
// children to find all the terminal nodes.
insertOrAppendPlacementNode(finishedWork, before, parent);
break;
}
case HostRoot:
case HostPortal: {
const parent: Container = parentFiber.stateNode.containerInfo;
const before = getHostSibling(finishedWork);
insertOrAppendPlacementNodeIntoContainer(finishedWork, before, parent);
break;
}
default:
throw new Error(
'Invalid host parent fiber. This error is likely caused by a bug ' +
'in React. Please file an issue.'
);
}
}
function insertOrAppendPlacementNode(
node: Fiber,
before: ?Instance,
parent: Instance
): void {
const { tag } = node;
const isHost = tag === HostComponent || tag === HostText;
if (isHost) {
const stateNode = node.stateNode;
if (before) {
insertBefore(parent, stateNode, before);
} else {
appendChild(parent, stateNode);
}
} else if (
tag === HostPortal ||
(supportsSingletons ? tag === HostSingleton : false)
) {
// If the insertion itself is a portal, then we don't want to traverse
// down its children. Instead, we'll get insertions from each child in
// the portal directly.
// If the insertion is a HostSingleton then it will be placed independently
} else {
const child = node.child;
if (child !== null) {
insertOrAppendPlacementNode(child, before, parent);
let sibling = child.sibling;
while (sibling !== null) {
insertOrAppendPlacementNode(sibling, before, parent);
sibling = sibling.sibling;
}
}
}
}