手写 mini-React实现多余子节点的删除逻辑

28 阅读2分钟

手写 React:实现多余子节点的删除逻辑

在构建自定义 React 渲染器时,节点更新的处理逻辑尤为关键。上一节我们已经实现了基本的节点更新,本节我们来处理一个更细致但非常常见的场景 —— 当新节点比旧节点少时,如何正确删除多余的旧子节点


一、问题现象:点击更新后旧节点未被删除

我们构建了一个简单的示例 App.tsx,点击触发更新,发现子节点没有被正确删除。如下图所示,旧的 child 仍然保留在 DOM 中。

1.png


二、图解 Fiber 更新问题

通过 Fiber 链表的结构可以看到:

  • 新的 Fiber 链表比旧的短;
  • 旧的多余子节点没有被处理。

2.png


三、打印旧 Fiber 节点定位问题

我们修改了 initChildren 方法,并打印出 oldFiber,发现它确实指向了多余的 DOM 节点:

console.log(`oldFiber fff`, oldFiber)

3.png


四、添加逻辑:检测并删除多余旧节点

我们开始加入判断,若旧 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
    }
}