今天带大家实现多个节点 diff。
先看个例子。
js
function FunctionComponent() {
const [count1, setCount1] = useReducer((x) => x + 1, 1);
const arr = count1 % 2 === 0 ? [0, 1, 2, 3, 4] : [3, 2, 0, 1, 4]
return (
<div>
<h3>函数组件</h3>
<button onClick={() => {setCount1()}}>{count1}</button>
<ul>
{arr.map((item) => {
<li key={"li" + item}>{item}</li>
})}
</ul>
</div>
)
}
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render((<FunctionComponent />) as any);
整体流程:
- 从左边往右遍历,比较新老节点,如果节点可以复用,继续往右,否则就停止
- 有三种互斥情况
- 新节点没了,老节点还有,则删除剩余的老节点即可
- 新节点还有,老节点没了,新增新节点
- 新老节点都还有节点,但是因为老 fiber 是链表,不方便快速 get 与 delete,因此把老 fiber 链表中的节点放入 Map 中,后续操作这个 Map 的 get 与 delete
- 如果是组件更新阶段,此时新节点已经遍历完了,能复用的老节点都用完了,则最后查找 Map 里是否还有元素,如果有,则证明是新节点里不能复用的,也就是要被删除的元素,此时删除这些元素就可以了。
对比 Vue3:
- 算法:React 没有复杂算法,Vue3 使用了最长递增子序列
- Map:构建的 map 的 value 不同:React 是 fiber,Vue3 是 index
- 数据结构:Vue3 有右边按序查找,React 没有,因为 Vue3 使用了数组,React 使用了单向链表

Render 阶段
BeginWork 阶段
入口函数是 reconcileChildrenArray
,增加了两次遍历。
js
function updateTextNode(
returnFiber: Fiber,
current: Fiber | null,
textContent: string
) {
if (current === null || current.tag !== HostText) {
// 老节点不是文本
const created = createFiberFromText(textContent);
created.return = returnFiber;
return created;
} else {
// 老节点是文本
const existing = useFiber(current, textContent);
existing.return = returnFiber;
return existing;
}
}
function updateElement(
returnFiber: Fiber,
current: Fiber | null,
element: ReactElement
) {
const elementType = element.type;
if (current !== null) {
if (current.elementType === elementType) {
// 类型相同
const existing = useFiber(current, element.props);
existing.return = returnFiber;
return existing;
}
}
const created = createFiberFromElement(element);
created.return = returnFiber;
return created;
}
function updateSlot(
returnFiber: Fiber,
oldFiber: Fiber | null,
newChild: any
) {
// 判断节点是否可以复用
const key = oldFiber !== null ? oldFiber.key : null;
if (isText(newChild)) {
if (key !== null) {
// 新节点是文本,老节点不是文本
return null;
}
// 有可能可以复用
return updateTextNode(returnFiber, oldFiber, newChild + "");
}
// 新节点不是文本
if (typeof newChild === "object" && newChild !== null) {
if (newChild.key === key) {
return updateElement(returnFiber, oldFiber, newChild);
} else {
return null;
}
}
// 兼容 null、undefined 的情况
return null;
}
function placeChild(
newFiber: Fiber,
lastPlacedIndex: number, // 记录的是新 fiber 在老 fiber 上的位置
newIndex: number
) {
newFiber.index = newIndex;
// 初次渲染
if (!shouldTrackSideEffects) {
return lastPlacedIndex;
}
// 判断节点位置是否发生相对位置变化,是否需要移动
const current = newFiber.alternate;
if (current !== null) {
const oldIndex = current.index;
if (oldIndex < lastPlacedIndex) {
// 0 1 2
// 0 2 1
// 节点需要移动位置
newFiber.flags |= Placement;
return lastPlacedIndex;
} else {
return oldIndex;
}
} else {
// 节点是新增
newFiber.flags |= Placement;
return lastPlacedIndex;
}
}
// 把剩下的节点构建成 map
function mapRemainingChildren(oldFiber: Fiber): Map<string | number, Fiber> {
const existingChildren: Map<string | number, Fiber> = new Map();
let existingChild: Fiber | null = oldFiber;
while (existingChild !== null) {
if (existingChild.key !== null) {
existingChildren.set(existingChild.key, existingChild);
} else {
existingChildren.set(existingChild.index, existingChild);
}
existingChild = existingChild.sibling;
}
return existingChildren;
}
function updateFromMap(
existingChildren: Map<string | number, Fiber>,
returnFiber: Fiber,
newIdx: number,
newChild: any
): Fiber | null {
if (isText(newChild)) {
// 文本节点没有 key 值,只能用 nexIdx
const matchedFiber = existingChildren.get(newIdx) || null;
return updateTextNode(returnFiber, matchedFiber, newChild + "");
} else if (typeof newChild === "object" && newChild !== null) {
const matchedFiber =
existingChildren.get(newChild.key === null ? newIdx : newChild.key) ||
null;
return updateElement(returnFiber, matchedFiber, newChild);
}
// 兼容 null、undefined 的情况
return null;
}
function reconcileChildrenArray(
returnFiber: Fiber,
currentFirstChild: Fiber | null,
newChildren: Array<any>
) {
let resultFirstChild: Fiber | null = null; // 头结点
let previousNewFiber: Fiber | null = null;
let oldFiber = currentFirstChild;
let nextOldFiber = null; // oldFiber.sibling
let newIdx = 0;
let lastPlacedIndex = 0;
// * 大多数实际场景下,节点相对位置不变
// 1. 从左往右遍历,按位置比较,如果可以复用,那就复用。不能复用,退出本轮
for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {
// 新旧节点位置不同
if (oldFiber.index > newIdx) {
nextOldFiber = oldFiber;
oldFiber = null;
} else {
nextOldFiber = oldFiber.sibling;
}
const newFiber = updateSlot(returnFiber, oldFiber, newChildren[newIdx]);
if (newFiber === null) {
// oldFiber.index > newIdx
if (oldFiber === null) {
oldFiber = nextOldFiber;
}
break;
}
// 更新
if (shouldTrackSideEffects) {
if (oldFiber && newFiber?.alternate === null) {
deleteChild(returnFiber, oldFiber);
}
}
// 判断节点在 DOM 的相对位置是否发生变化
// 组件更新阶段,判断在更新前后的位置是否一致,如果不一致,需要移动
lastPlacedIndex = placeChild(newFiber as Fiber, lastPlacedIndex, newIdx);
if (previousNewFiber === null) {
// 第一个节点,不要用 newIdx 判断,因为有可能有 null,而 null 不是有效 fiber
resultFirstChild = newFiber as Fiber;
} else {
previousNewFiber.sibling = newFiber as Fiber;
}
previousNewFiber = newFiber as Fiber;
oldFiber = nextOldFiber;
}
// * Vue 1.2 从右往左遍历,按位置比较,如果可以复用,那就复用。不能复用,退出本轮
// 2.1 老节点还有,新节点没了。删除剩余的老节点
// 新节点遍历完了
if (newIdx === newChildren.length) {
deleteRemainingChildren(returnFiber, oldFiber);
return resultFirstChild;
}
// 2.2 新节点还有,老节点没了。剩下的新节点新增就可以了
// 包括页面初次渲染
if (oldFiber === null) {
for (; newIdx < newChildren.length; newIdx++) {
const newFiber = createChild(returnFiber, newChildren[newIdx]);
// null、undefined
if (newFiber === null) {
continue;
}
// 组件更新阶段,判断在更新前后的位置是否一致,如果不一致,需要移动
lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
if (previousNewFiber === null) {
// 第一个节点,不要用 newIdx 判断,因为有可能有 null,而 null 不是有效 fiber
resultFirstChild = newFiber;
} else {
previousNewFiber.sibling = newFiber;
}
previousNewFiber = newFiber;
}
return resultFirstChild;
}
// 2.3 新老节点都还有
// 构建 map
const existingChildren = mapRemainingChildren(oldFiber);
for (; newIdx < newChildren.length; newIdx++) {
const newFiber = updateFromMap(
existingChildren,
returnFiber,
newIdx,
newChildren[newIdx]
);
if (newFiber !== null) {
// 更新
if (shouldTrackSideEffects) {
// 已经复用过了,删除
// 每个节点只能被复用一次
existingChildren.delete(
newFiber.key === null ? newIdx : newFiber.key
);
}
lastPlacedIndex = placeChild(
newFiber as Fiber,
lastPlacedIndex,
newIdx
);
if (previousNewFiber === null) {
// 第一个节点,不要用 newIdx 判断,因为有可能有 null,而 null 不是有效 fiber
resultFirstChild = newFiber;
} else {
previousNewFiber.sibling = newFiber;
}
previousNewFiber = newFiber;
}
}
// 3. 如果新节点已经构建完了,但是老节点还有
if (shouldTrackSideEffects) {
existingChildren.forEach((child) => deleteChild(returnFiber, child));
}
return resultFirstChild;
}
function reconcileChildFibers(
returnFiber: Fiber,
currentFirstChild: Fiber | null,
newChild: any
) {
if (isText(newChild)) {
return placeSingleChild(
reconcileSingleTextNode(returnFiber, currentFirstChild, newChild + "")
);
}
// 检查newChild类型,单个节点、文本、数组
if (typeof newChild === "object" && newChild !== null) {
switch (newChild.$$typeof) {
case REACT_ELEMENT_TYPE: {
return placeSingleChild(
reconcileSingleElement(returnFiber, currentFirstChild, newChild)
);
}
}
}
// 子节点是数组
if (isArray(newChild)) {
return reconcileChildrenArray(returnFiber, currentFirstChild, newChild);
}
// todo
return null;
}
return reconcileChildFibers;
}
Commit 阶段
commitPlacement
不能单纯的 appendChild
了,要区分 insertBefore
和 appendChild
,便于实现移动。
js
// fiber.stateNode 是 DOM 节点
// 有 dom 节点且能兄弟节点
export function isHost(fiber: Fiber): boolean {
return fiber.tag === HostComponent || fiber.tag === HostText;
}
function getHostSibling(fiber: Fiber) {
let node = fiber;
// label 函数
sibling: while (1) {
while (node.sibling === null) {
if (node.return === null || isHostParent(node.return)) {
return null;
}
node = node.return;
}
// todo
node = node.sibling;
while (!isHost(node)) {
// 新增插入|移动位置
// 兄弟节点不能移动,必须稳定
if (node.flags & Placement) {
continue sibling;
}
// 比如函数组件,往里继续查找
if (node.child === null) {
continue sibling;
} else {
node = node.child;
}
}
// HostComponent|HostText
if (!(node.flags & Placement)) {
return node.stateNode;
}
}
}
// 新增插入 | 位置移动
// insertBefore | appendChild
function insertOrAppendPlacementNode(
node: Fiber,
before: Element,
parent: Element
) {
if (before) {
parent.insertBefore(getStateNode(node), before);
} else {
parent.appendChild(getStateNode(node));
}
}
function commitPlacement(finishedWork: Fiber) {
if (finishedWork.stateNode && isHost(finishedWork)) {
// finishedWork 是有 dom 节点
const domNode = finishedWork.stateNode
// 找 domNode 的父 DOM 节点对应的 fiber
const parentFiber = getHostParentFiber(finishedWork);
let parentDom = parentFiber.stateNode;
if (parentDom.containerInfo) {
// HostRoot
parentDom = parentDom.containerInfo;
}
// parentDom.appendChild(domNode)
// 遍历 fiber,寻找 finishedWork 的兄弟节点,并且这个 sibling 有 dom 节点,且是更新的节点(上一轮)。在本轮不发生移动
const before = getHostSibling(finishedWork);
insertOrAppendPlacementNode(finishedWork, before, parentDom);
} else {
// Fragment
let kid = finishedWork.child;
while (kid !== null) {
commitPlacement(kid);
kid = kid.sibling;
}
}
}