手写 React:实现多余子节点的删除逻辑
在构建自定义 React 渲染器时,节点更新的处理逻辑尤为关键。上一节我们已经实现了基本的节点更新,本节我们来处理一个更细致但非常常见的场景 —— 当新节点比旧节点少时,如何正确删除多余的旧子节点。
一、问题现象:点击更新后旧节点未被删除
我们构建了一个简单的示例 App.tsx
,点击触发更新,发现子节点没有被正确删除。如下图所示,旧的 child
仍然保留在 DOM 中。
二、图解 Fiber 更新问题
通过 Fiber 链表的结构可以看到:
- 新的 Fiber 链表比旧的短;
- 旧的多余子节点没有被处理。
三、打印旧 Fiber 节点定位问题
我们修改了 initChildren
方法,并打印出 oldFiber
,发现它确实指向了多余的 DOM 节点:
console.log(`oldFiber fff`, oldFiber)
四、添加逻辑:检测并删除多余旧节点
我们开始加入判断,若旧 Fiber 不为空,则加入 deletions
队列:
if (oldFiber) {
deletions.push(oldFiber)
}
但这样仅能处理一个额外子节点的场景。
五、处理多个子节点的删除
假设我们更新后的节点从两个子节点变为一个子节点:
const Foo = <div>
foo
<div>child1</div>
<div>child2</div>
</div>
const Bar = <div>bar</div>
这时 child2
并未删除。我们需要用 while 循环 把所有剩下的 oldFiber.sibling
全部加入 deletions
队列:
while (oldFiber) {
deletions.push(oldFiber)
oldFiber = oldFiber.sibling
}
六、完整的 initChildren
更新逻辑如下
function initChildren(fiber, children) {
let oldFiber = fiber.alternate?.child
let prevChild = null
children.forEach((child, index) => {
const isSameType = oldFiber && oldFiber.type === child.type
let newFiber
if (isSameType) {
newFiber = {
type: child.type,
props: child.props,
child: null,
parent: fiber,
sibling: null,
dom: oldFiber.dom,
effectTag: "update",
alternate: oldFiber
}
} else {
newFiber = {
type: child.type,
props: child.props,
child: null,
parent: fiber,
sibling: null,
dom: null,
effectTag: "placement"
}
if (oldFiber) {
deletions.push(oldFiber)
}
}
if (oldFiber) {
oldFiber = oldFiber.sibling
}
if (index === 0) {
fiber.child = newFiber
} else {
prevChild.sibling = newFiber
}
prevChild = newFiber
})
// 删除剩余 oldFiber 节点
while (oldFiber) {
deletions.push(oldFiber)
oldFiber = oldFiber.sibling
}
}