React源码解析(五):reconcileChildren源码解析

263 阅读3分钟

reconcileChildren

主要作用是动态比较已缓存的虚拟 DOM 树与新的 element 对象,生成 workInProgress 的所有子节点, 没有缓存说明是新节点,会直接创建。

function reconcileChildren(current, workInProgress, nextChildren, renderLanes) {
    if (current === null) {
        workInProgress.child = mountChildFibers(workInProgress, null, nextChildren, renderLanes);
    } else {
        workInProgress.child = reconcileChildFibers(workInProgress, current.child, nextChildren, renderLanes);
    }
}
// reconcileChildrenFibers 和 mountChildFibers 来源
export const reconcileChildFibers = ChildReconciler(true);
export const mountChildFibers = ChildReconciler(false);
// shouldTrackSideEffects 标识, 是否为 Fiber 对象添加 effectTag
// true 添加 false 不添加
// 对于初始渲染来说, 只有根组件需要添加, 其他元素不需要添加, 防止过多的 DOM 操作
function ChildReconciler(shouldTrackSideEffects) {
    ...
    return reconcileChildFibers;
}

reconcileChildFibers

先判断是否为 nextChild 是否为占位组件,如果是则将其子组件设置为 newChild

const isUnkeyedTopLevelFragment =
    typeof newChild === 'object' &&
    newChild !== null &&
    newChild.type === REACT_FRAGMENT_TYPE &&
    newChild.key === null;
if (isUnkeyedTopLevelFragment) {
    newChild = newChild.props.children;
}
  • 之后会根据 newChild 的类型,执行不同的 reconcile 过程;
newChild 为对象
  • newChild.$$typeof === REACT_ELEMENT_TYPE
placeSingleChild(
    reconcileSingleElement(
        returnFiber,
        currentFirstChild,
        newChild,
        lanes,
    ),
);
  • newChild.$$typeof === REACT_PORTAL_TYPE
placeSingleChild(
    reconcileSinglePortal(
        returnFiber,
        currentFirstChild,
        newChild,
        lanes,
    ),
);
  • newChild.$$typeof === REACT_LAZY_TYPE
const payload = newChild._payload
const init = newChild._init
// 重新执行 reconcileChildFibers 函数
reconcileChildFibers(
    returnFiber,
    currentFirstChild,
    init(payload),
    lanes,
);
newChild 为数组
reconcileChildrenArray(
    returnFiber,
    currentFirstChild,
    newChild,
    lanes,
);
newChild 为迭代器对象
reconcileChildrenIterator(
    returnFiber,
    currentFirstChild,
    newChild,
    lanes,
);
newChildstring 或者 number
placeSingleChild(
    reconcileSingleTextNode(
        returnFiber,
        currentFirstChild,
        '' + newChild,
        lanes,
    ),
);

reconcileSingleElement

  • 循环遍历旧的子节点链表 currentFirstChild 查找 key 与新的子节点 child.key 相似的节点;
  • key 相等,克隆复用 currentFirstChild 中当前节点,删除剩余节点;
  • key 不相等,删除 currentFirstChild 中当前节点,查看下一个兄弟节点;
let child = currentFirstChild;
while (child !== null) {
    // child为旧的Fiber newChild为新的JSX element对象
    if (child.key === newChild.key) {
        // 克隆复用并返回旧节点
        // 删除剩余节点
        break
    } else {
        // 删除当前节点
    }
    child = child.sibling;
}
  • 循环中未能找到相同 key 的节点,因此需要创建一个新的节点并返回;
const created = createFiberFromFragment(
    newChild.props.children,
    returnFiber.mode,
    lanes,
    newChild.key
)
created.return = returnFiber
return created

reconcileChildrenArray

对数组做 reconcile,子 Fiber 链表(用 sibling 连接)节点称为 oldChild,子元素数组中的每一个 element 对象为 newChild

  • oldChild 不为 null 的前提下循环遍历 newChildren 数组;
  • 执行 updateSlot 函数试图复用 oldFiber,如果 key 相同则返回复用的 oldFiber,如果 key 不同则返回 null,退出循环
  • 到此步说明复用节点成功;如果不是首次渲染,则从父节点中删除此 oldFiber
  • newFiber 插入到新子节点 Fiber 链表的最后;
  • 指定下一个 oldFiber 继续循环;
for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {
    const newFiber = updateSlot(
        returnFiber,
        oldFiber,
        newChildren[newIdx],
        lanes
    )
    if (newFiber === null) break
    if (shouldTrackSideEffects) deleteChild(returnFiber, oldFiber)

    if (previousNewFiber === null) resultingFirstChild = newFiber;
    else previousNewFiber.sibling = newFiber;
    previousNewFiber = newFiber
    oldFiber = oldFiber.index > newIdx ? oldFiber : oldFiber.sibling
}
  • 如果 newIdx === newChildren.length,则删除剩余的 oldFiber,返回 resultingFirstChild;
  • 如果 oldFiber === null,继续循环遍历 newChildren 数组,为每一个新的 element 对象创建新的 Fiber 节点,并将这些 newFiber 也插入到新子节点 Fiber 链表的最后,返回 resultingFirstChild
if (newIdx === newChildren.length) {
    deleteRemainingChildren(returnFiber, oldFiber);
    return resultingFirstChild;
}

if (oldFiber === null) {
    for (; newIdx < newChildren.length; newIdx++) {
        const newFiber = createChild(returnFiber, newChildren[newIdx], lanes);
        if (newFiber === null) {
            continue;
        }
        if (previousNewFiber === null) resultingFirstChild = newFiber;
        else previousNewFiber.sibling = newFiber;
        previousNewFiber = newFiber;
    }
    return resultingFirstChild;
}
  • 到此步骤说明 oldFibernewChildren 仍与剩余,因此,先为 oldFiber 创建一个 map,键为 oldFiberkey 或者 index,值为 oldFiber 的本身;
  • 遍历剩余的 newChildren 数组,根据 keyindexmap 中查找可复用的 Fiber 节点,如果存在可复用 newFiber,则将 newFiber 插入到新子节点 Fiber 链表的最后。
const existingChildren = mapRemainingChildren(returnFiber, oldFiber);
for (; newIdx < newChildren.length; newIdx++) {
    const newFiber = updateFromMap (
        existingChildren,
        returnFiber,
        newIdx,
        newChidren[newIdx],
        lanes
    )
    if (newFiber !== null) {
        if (previousNewFiber === null) resultingFirstChild = newFiber;
        else previousNewFiber.sibling = newFiber;
        previousNewFiber = newFiber
    }
}

最终返回resultingFirstChild