代码仓库👉mini-react,本节代码在 v0.0.4 分支
在上一节我们实现了useReduer和useState来更新数据,但是当我们需要根据数据来决定应该渲染怎样的dom内容时却发现出现了一些问题。
如下代码。当count % 2时使用<div>学习React</div> 否则使用<span>学习算法</span>
function FunctionComponent(props) {
const [count, setCount] = useState(0)
return (
<div className='function'>
<div>
<button onClick={() => setCount(count + 1)}>count值 + 1</button>
</div>
<div>{count}</div>
{count % 2 ? <div>学习React</div> : <span>学习算法</span>}
</div>
)
}
渲染到页面上,点击按钮会发现原本需要删除的节点没有被删除掉,反而不停的增加新的节点。我们处理一下在reconcileChildren中不能复用的情况——也就是将不能复用的节点删除。
当然我们这里并不是真正的删除,只是将需要删除的节点收集到 父fiber 的 deletions 数组中
// src/ReactFiberReconciler.ts
// 将需要删除的 fiber 添加到 父fiber 的 deletions 数组中
function deleteChild(returnFiber: Fiber, childToDelete: Fiber) {
if (returnFiber.deletions) {
returnFiber.deletions.push(childToDelete)
} else {
returnFiber.deletions = [childToDelete]
}
}
function reconcileChildren(workInProgress: Fiber, children) {
if (isStringOrNumber(children)) {
return
}
const newChildren: any[] = isArray(children) ? children : [children]
let oldFiber = workInProgress.alternate?.child
let previousNewFiber: Fiber | null = null
for (let i = 0; i < newChildren.length; i++) {
const newChild = newChildren[i]
if (newChild === null) {
continue
}
const newFiber = createFiber(newChild, workInProgress)
// 能否复用
const same = sameNode(newFiber, oldFiber)
if (same) {
// 能复用
Object.assign(newFiber, {
stateNode: (oldFiber as Fiber).stateNode,
alternate: oldFiber as Fiber,
flags: Update
})
}
// 不能复用的老节点需要删除
if (!same && oldFiber) {
deleteChild(workInProgress, oldFiber)
}
if (oldFiber) {
oldFiber = oldFiber.sibling
}
if (previousNewFiber === null) {
workInProgress.child = newFiber
} else {
previousNewFiber.sibling = newFiber
}
previousNewFiber = newFiber
}
}
收集到returnFiber.delations属性中以后,我们需要在commit时操作真正的DOM,将这些节点删除掉。
// src/ReactWorkLoop.ts
function commitWorker(workInProgress: Fiber | null) {
if (!workInProgress) return
let parentNode = getParentNode(workInProgress.return)
const { flags, stateNode } = workInProgress
if (flags & Placement && stateNode) {
parentNode.appendChild(stateNode)
}
if (flags & Update && stateNode) {
updateNode(
stateNode,
(workInProgress.alternate as Fiber).props,
workInProgress.props
)
}
// 如果有 deletions 属性,说明有节点需要被删除掉
if (workInProgress.deletions) {
commitDeletions(workInProgress.deletions, stateNode || parentNode)
}
commitWorker(workInProgress.child)
commitWorker(workInProgress.sibling)
}
function commitDeletions(deletions: , parentNode: HTMLElement) {
for(let i = 0; i < deletions.length; i++) {
parentNode.removeChild(getStateNode(deletions[i]))
}
}
// function组件 或 fragment组件需要往下找原生组件
function getStateNode(fiber: Fiber) {
let temp: Fiber | null = fiber
while(!temp?.stateNode) {
temp = temp?.child || null
}
return temp.stateNode
}
删除多个节点
上面的删除单个节点还行,但是如果将代码改成如下,count默认为4,每次点击-2这样也就意味着每次点击(不算复原的setCount(4))都将删除最后两个节点(多节点删除)。
function FunctionComponent(props) {
const [count, setCount] = useState(4)
const handler = () => {
if (count === 0) {
setCount(4)
} else {
setCount(count - 2)
}
}
return (
<div className='function'>
<div>
<button onClick={handler}>{count}</button>
<ul>
{
[0, 1, 2, 3, 4].map(item => count >= item ? <li key={item}>{item}</li> : null)
}
</ul>
</div>
</div>
)
}
在浏览器中跑发现并没有如我们的预期那样删除。接下来我们实现多节点删除。
将原本的for (let i = 0; i < newChildren.length newIndex++) { /.../ } 中的i提升到reconcileChildren中,变量名更改为newIndex。
遍历完以后,比较newIndex是否与newChildren.length相等,如果相等则说明新的节点已经全部复用完,可能后面还有兄弟节点们需要删除,那么只需要将剩余的节点删除即可。
// 删除多个节点
function deleteRemainingChildren(returnFiber: Fiber, currentFirstChild: Fiber | null | undefined) {
let childToDelete = currentFirstChild
while(childToDelete) { // 遍历链表
deleteChild(returnFiber, childToDelete)
childToDelete = childToDelete.sibling
}
}
function reconcileChildren(returnFiber: Fiber, children) {
if (isStringOrNumber(children)) {
return
}
const newChildren: any[] = isArray(children) ? children : [children]
let oldFiber = returnFiber.alternate?.child
let previousNewFiber: Fiber | null = null
let newIndex = 0
for (; newIndex < newChildren.length; newIndex++) {
const newChild = newChildren[newIndex]
if (newChild === null) {
continue
}
const newFiber = createFiber(newChild, returnFiber)
const same = sameNode(newFiber, oldFiber)
if (same) {
Object.assign(newFiber, {
stateNode: (oldFiber as Fiber).stateNode,
alternate: oldFiber as Fiber,
flags: Update
})
}
if (!same && oldFiber) {
deleteChild(returnFiber, oldFiber)
}
if (oldFiber) {
oldFiber = oldFiber.sibling
}
if (previousNewFiber === null) {
returnFiber.child = newFiber
} else {
previousNewFiber.sibling = newFiber
}
previousNewFiber = newFiber
}
if (newIndex === newChildren.length) {
// 已经到了新 child 的尽头,可以删除其余的节点了
deleteRemainingChildren(returnFiber, oldFiber)
return
}
}
我们再回到浏览器中进行操作,就会发现展示结果与我们的预期一致了。实际上在 react 中实现节点的删除与更新比这复杂得多,我们这里的节点复用只有前面的节点对比相等才可以复用,但是实际场景中,在多个节点中,可能发生位移,或者删除单个节点,那么此时我们实现的reconcileChidlren就不能做到节点复用了,这就造成了浪费。
下一节,我们一步一步的实现 react 中的 diff 算法。