引言

本文对应的 react 版本是 18.2.0

下面的 dom 结构react 内部是如何遍历的

const App = () => {
  return (
    <div>
      <button>+1</button>
      <A count={0} />
    </div>
  );
};
const A = (props) => {
  useEffect(() => {
    console.log(props.count);
  }, [props.count]);
  return <div>{props.count}</div>;
};

react 内部遍历核心逻辑:

  • render 时调用 commitPassiveUnmountOnFiber 函数
  • commitPassiveUnmountOnFiber 处理不同的 WorkTag,并调用 recursivelyTraversePassiveUnmountEffects
  • recursivelyTraversePassiveUnmountEffects 根据当前 Fiber 的子节点有没有 passive effectuseEffectuseLayoutEffect)来决定是否遍历当前 Fiber 的子节点
    • 如果子节点有 passive effect,则优先遍历子节点 (深度优先),直到找到最终的叶子节点,退出当前循环
    • 然后进入兄弟节点,开始遍历兄弟节点的子节点
      • 具体从哪个兄弟节点开始遍历,react 选择的是离退出循环的那个叶子节点的父节点,检查有没有子节点,以此循环遍历
    • 直到最后找到所有有 passive effect 的节点

代码简化:

commitPassiveUnmountOnFiber(root.current);
function commitPassiveUnmountOnFiber(finishedWork) {
  // 省略了处理不同的 WorkTag
  recursivelyTraversePassiveUnmountEffects(finishedWork);
}
function recursivelyTraversePassiveUnmountEffects(parentFiber) {
  // 省略了其他处理
  if (parentFiber.subtreeFlags & PassiveMask) {
    let child = parentFiber.child;
    while (child !== null) {
      commitPassiveUnmountOnFiber(child);
      child = child.sibling;
    }
  }
}

所以对于这段 dom 的遍历逻辑是:

  • 首先从根组件开始 FiberRootNode,取到 current
    • 也就是说 FiberRootNode.currentdiv#root 这是一个 fiber,它的 tag3
  • 由于 App 的子组件有 passive effect,所以会进入 App 组件,它的 tag0
  • App 组件中节点是 <div><di >tag5
    • <div> 下面有两个子元素 <button><A>
  • 先遍历 <button> 它的 tag5
  • <button> 内部只有一个文本节点,没有 passive effect
    • 所以 react 不遍历了(跳出当前遍历的循环,也就是 button 这条不在遍历了)
  • 跳出循环后,查看 button 的兄弟节点,它的兄弟节点是 <A><A>tag0
  • 由于 <A> 节点的子节点没有 passive effect,所以跳出循环,结束整个遍历

总结

  • 从跟节点开始遍历
  • 当前组件的子组件有没有 passive effect
  • 采取深度优先
  • 如果 dom 节点内有函数组件,则这个 dom 会被遍历,否则不会遍历
  • 如果当前 fiber 下的所有子 fiber 都没有 passive effect ,则这一整个都链表都不会被遍历
  • 如果当前 fiber 只有 dom,则这些 dom 也不会遍历

总的来说组件会不会别遍历看 fiber 有没有 passive effect

  • 有,一定会被遍历
  • 没有,下面两种情况会被遍历,其他情况不会被遍历
    • passive effect 的父组件
    • passive effect 组件是兄弟组件

passive effect 指的是 useEffectuseLayoutEffect

遍历逻辑如下图所示

图中画绿色勾的都会被遍历,红色勾是遍历的顺序