处理 Mini React 中的 Edge Case:条件渲染中的 false 报错问题

16 阅读2分钟

在构建一个简易版 React 框架的过程中,我们常常会遇到一些边缘情况(edge case),本次课程聚焦于其中一种常见但容易忽略的情况:条件渲染返回 false 时导致 Fiber 构建报错的问题。以下是详细分析与修复过程。

🧩 问题复现

我们从一个组件 Counter 开始:

let showBar = false;
​
function Counter({ num }) {
  const bar = <div>bar</div>;
​
  function handleShowBar() {
    showBar = !showBar;
    React.update();
  }
​
  return (
    <div>
      Counter
      <button onClick={handleShowBar}>showBar</button>
      {showBar && bar}
    </div>
  );
}

初看之下并无问题,但运行时报错。我们追踪到 createElement 函数中的调试信息:

console.log('child 111', child)

打印发现 childfalse,这是因为 JSX 中 showBar && barshowBar === false 时会返回布尔值 false,而我们的框架没有处理布尔类型子节点。

✅ 问题定位与第一次修复

原先的处理逻辑只考虑了文本节点和对象节点:

const isTextNode = typeof child === "string" || typeof child === "number"
return isTextNode ? createTextNode(child) : child

我们增加一个布尔值的处理判断(过滤掉 false, null, undefined 等):

children: children
  .filter(child => child != null && child !== false)
  .map(child => {
    const isTextNode = typeof child === "string" || typeof child === "number";
    return isTextNode ? createTextNode(child) : child;
  })

或在 reconcileChildren 中增加:

if (!child) return;

这样,当 showBarfalse 时,不再处理布尔值。

🔄 问题延伸:变更顺序导致报错

我们继续尝试调整元素顺序:

return (
  <div>
    Counter
    {showBar && bar}
    <button onClick={handleShowBar}>showBar</button>
  </div>
);

此时再次报错,错误原因是 button 组件在当前 Fiber Tree 中找不到旧的对应节点。追踪代码,发现问题出在 prevChild 的赋值:

if (index === 0) {
  fiber.child = newFiber;
} else {
  prevChild.sibling = newFiber; // ❗ newFiber 可能是 undefined
}
prevChild = newFiber;

修复方法:确保只有 newFiber 存在时才赋值给 prevChild

if (newFiber) {
  if (index === 0) {
    fiber.child = newFiber;
  } else if (prevChild) {
    prevChild.sibling = newFiber;
  }
  prevChild = newFiber;
}

这样处理后,无论组件顺序如何调整,Fiber 树都能正确生成。


✅ 最终修复后的 reconcileChildren 关键逻辑

function reconcileChildren(fiber, children) {
  let oldFiber = fiber.alternate?.child;
  let prevChild = null;
​
  children.forEach((child, index) => {
    if (!child) return;
​
    const isSameType = oldFiber && oldFiber.type === child.type;
    let newFiber;
​
    if (isSameType) {
      newFiber = {
        type: child.type,
        props: child.props,
        dom: oldFiber.dom,
        parent: fiber,
        alternate: oldFiber,
        effectTag: 'update',
        child: null,
        sibling: null,
      };
    } else {
      newFiber = {
        type: child.type,
        props: child.props,
        dom: null,
        parent: fiber,
        effectTag: 'placement',
        child: null,
        sibling: null,
      };
      if (oldFiber) deletions.push(oldFiber);
    }
​
    if (oldFiber) oldFiber = oldFiber.sibling;
​
    if (newFiber) {
      if (index === 0) {
        fiber.child = newFiber;
      } else if (prevChild) {
        prevChild.sibling = newFiber;
      }
      prevChild = newFiber;
    }
  });
​
  while (oldFiber) {
    deletions.push(oldFiber);
    oldFiber = oldFiber.sibling;
  }
}