mini-react 实现组件更新与删除机制详解:type 不一致即替换
在构建 mini-react
的过程中,组件的 更新与删除机制 是核心功能之一。特别是在虚拟 DOM diff 过程中,当新旧节点的 type
不一致时,应当删除旧节点并创建新节点。本文将通过示例和源码解析,带你实现一个基础的节点替换逻辑,包括对函数组件的特殊处理。
🎯 场景说明:div
替换为 p
我们通过一个切换组件的小例子来演示替换的过程。初始展示 div
,点击按钮后变为 p
标签。
测试代码:App.jsx
tsx
import React from './core/React.js';
let showBar = false
function Counter({ num }) {
const Foo = <div>foo</div>
const Bar = <p>bar</p>
function handleShowBar() {
showBar = !showBar
React.update()
}
return <div>
Counter
<div>{showBar ? Bar : Foo}</div>
<button onClick={handleShowBar}>show bar</button>
</div>
}
function App() {
return <div>
hi-mini-react
<Counter></Counter>
</div>
}
export default App;
运行上述代码时发现:点击按钮后,并未删除旧节点,仅简单添加了新节点。
🧩 删除策略:收集后统一删除
我们需要在 initChildren
中识别出类型不一致的旧节点,并将其添加到 deletions
数组中,在 commitRoot
中统一删除。
修改 initChildren
tsx
function initChildren(fiber, children) {
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) {
console.log('oldFiberShouldDelete', oldFiber)
deletions.push(oldFiber)
}
}
})
}
🧹 commit 阶段统一删除
在 commitRoot
中统一执行删除操作:
tsx
function commitRoot() {
deletions.forEach(commitDeletion)
commitWork(wipRoot.child)
currentRoot = wipRoot
wipRoot = null
deletions = []
}
function commitDeletion(fiber) {
fiber.parent.dom.removeChild(fiber.dom)
}
此时效果正常,div
能被成功替换为 p
。
⚠️ 处理函数组件:fiber.dom 为 null 问题
将 Foo
和 Bar
改为函数组件:
tsx
function Counter({ num }) {
const Foo = () => <div>foo</div>
const Bar = () => <p>bar</p>
function handleShowBar() {
showBar = !showBar
React.update()
}
return <div>
Counter
<div>{showBar ? <Bar /> : <Foo />}</div>
<button onClick={handleShowBar}>show bar</button>
</div>
}
点击按钮时报错:fiber.dom is null
。这是因为函数组件本身没有 DOM 节点,因此无法直接删除。
修复方案:递归向下查找有 dom 的子节点
tsx
function commitDeletion(fiber) {
let fiberParent = fiber.parent
while (!fiberParent.dom) {
fiberParent = fiberParent.parent
}
if (fiber.dom) {
fiberParent.dom.removeChild(fiber.dom)
} else {
commitDeletion(fiber.child)
}
}
此修改后,函数组件下的真实 DOM 也能被正确移除。
✅ 总结(100字)
本文讲解了在 mini-react
中实现组件更新和删除的过程。通过比较旧新节点的 type
实现替换策略,借助 deletions
队列实现统一删除,并对函数组件做了特殊处理,确保无 DOM 的节点也能正确卸载,完整实现一个基础但关键的 diff 更新逻辑。