Skip to main content

第 10 章:特殊组件——没有 UI 的"旁路指令"

第 6 章说过,beginWorktag 分派。前几章看了普通组件(函数、DOM、根节点),本章看剩下的特殊类型:FragmentmemoContext.Provider/ConsumerSuspenseLazy

它们有一个共同点:自己不产生任何 UI<Fragment> 不会变成 DOM,<memo> 不是真的组件,Context.Provider 只是个开关。它们全是旁路指令:告诉 React「我的子节点该怎么特殊处理」。

先记住这一句:特殊组件没有 UI,它们改变的是 reconciler 对子节点的处理路径。看懂了这一点,整类组件就只剩"每条旁路改了什么"的问题。

一图流:特殊组件各管什么

Fragment:最透明的一个

updateFragmentReactFiberBeginWork.js:1333)短得惊人:

const nextChildren = workInProgress.pendingProps;
reconcileChildren(current, workInProgress, nextChildren, renderLanes);
return workInProgress.child;

拿到 children,reconcileChildren 一下,完事。Fragment 就是一层不存在的壳,它的 children 直接挂到父节点名下,不产生 DOM,也几乎不产生额外 fiber 开销(对应 fiber 的 completeWork 也直接冒泡,第 8 章)。它的作用就是"给一组元素一个不用 DOM 的容器"。

memo:把第 6 章的 bailout 变成你手上的开关

updateMemoComponentReactFiberBeginWork.js:472)做的事,就是给组件套一层"先比较、再决定要不要渲染"的检查。核心只有一段(520):

const compare = Component.compare !== null ? Component.compare : shallowEqual;
if (compare(prevProps, nextProps) && current.ref === workInProgress.ref) {
return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
}

memo 的机制就是第 6 章那个 bailout。默认用 shallowEqual 浅比较新旧 props,相等就直接 bailoutOnAlreadyFinishedWork3714),整棵子树跳过,组件函数根本不调用。你传的第二个参数 memo(Comp, myCompare) 里的 myCompare,就是这里的 Component.compare

还有一个值得知道的内部优化:如果你 memo 的是一个简单函数组件且没传比较函数,React 首次挂载时会把它的 tag 升级成 SimpleMemoComponent489),走更快的 updateSimpleMemoComponent540)。它省掉一层包装 fiber,比较逻辑几乎一样。

Context:Provider 压栈,Consumer 读栈

Context 的实现是"值栈 + 依赖订阅"。

ProviderupdateContextProvider3607)把自己包住的值压进一个 context 栈,然后照常渲染 children:

pushProvider(workInProgress, context, newValue);
const newChildren = newProps.children;
reconcileChildren(current, workInProgress, newChildren, renderLanes);

ConsumerupdateContextConsumer3634)用 readContext(context) 从栈里取值,并把"这个消费者依赖了哪个 context"记进 fiber 的 dependencies。下次 Provider 值变了,React 就能顺着 dependencies 找到所有该重渲染的 Consumer,哪怕中间隔着 memo。这是第 15 章(useContext)的主线,这里先留个印象:Provider 只管压值,Consumer 负责订阅

Lazy 与 Suspense:先占个座

mountLazyComponent2077)处理 lazy(() => import(...)):首次渲染时组件还没加载完,它抛出那个 promise,让 Suspense 抓住显示 fallback;加载完再回来真渲染。Suspense 的 updateSuspenseComponent2342)在这里只做"识别挂起 + 切 fallback"。这两个的完整机制(挂起、恢复、并发下的行为)是第 26 章的主场,这里知道"Lazy 负责等加载,Suspense 负责占位"就够了。

动手实验

  1. 扒开 Fragment<><span/>...</> 渲染后,在 DevTools 里看它对应的 fiber 有没有 DOM。再在 updateFragment(ReactFiberBeginWork.js:1333)打断点,看它是不是真的"拿到 children 就转手"。
  2. 验证 memo 的 bailoutconst M = memo(Counter);,父组件每次重渲染,在 Counter 函数体第一行打断点。props 没变时,Counter 根本不会被执行,断点不命中。这就是 shallowEqual → bailout。
  3. 对比 memo 和 useMemo 的对象<M someObj={{a:1}} /> 每次传新对象,shallowEqual 对引用不同就判"变了",memo 失效。体会"浅比较只比引用"的边界。

小结

  • 特殊组件没有 UI,全是旁路指令,改的是 reconciler 处理子节点的路径。
  • Fragment 是透明壳,直接转发 children。
  • memo 是第 6 章 bailout 的公开开关,shallowEqual 过了就跳过整棵子树;简单函数组件会升级成 SimpleMemoComponent 快路径。
  • Context:Provider 压值进栈,Consumer 读值并登记依赖,中间隔着 memo 也能直达。
  • Lazy / Suspense:负责等加载和占位,完整机制在第 26 章。
  • 下一步:第二部分收尾。第 11 章看一眼 ClassComponent(19 的遗留支持,选讲)。