
先分清三层来源
这一篇讲的是 React 的内部实现,而官方文档没有这部分内容:react.dev 上没有 Fiber 页,没有调度器页,也没有 diff 算法页。
所以本文按三层来写:官方能查到的行为放第一层,社区对内部结构的重构放第二层,教学实现的代码放第三层。每一层都标出出处,免得把第三层当成规范去背。
第一层:官方能查到的
一次更新分触发、渲染、提交三步。渲染阶段只算出要改什么,提交阶段才应用,而且「应用的是必要的最小改动,这些改动是渲染期间算出来的」(Render and Commit)。两次渲染结果相同,React 就不碰 DOM。
涉及「能不能被打断」,官方只在并发特性那几页讲行为:过渡更新可被打断(useTransition)。
涉及「哪些节点被复用、state 归谁」,官方现在的讲法是位置与身份:同一个位置上的同一个组件保留 state,key 用来改变这个判断(Preserving and Resetting State)。
第二层:社区的重构
这一层最常被引用的来源是 Andrew Clark 的 react-fiber-architecture。他在 README 里写明「我不在 React 团队,不代表任何权威,这不是官方文档」,并自述这份文档远未完成;仓库在 2024 年 7 月之后没有更新。
模型大致是这样:
- 旧架构靠递归做 diff,一旦开始就不能暂停,树一大就长时间占住主线程。
- 新架构把元素树转成一棵由 Fiber 节点组成的树,每个节点是最小工作单元,用
child、sibling、return三个指针串起来。遍历随时可以停下来,下次从停下的地方继续。 - 渲染阶段可中断,提交阶段不可中断。
- 内存里同时存在两棵树:
current是屏幕上那一棵,workInProgress是正在算的那一棵,两者的同名节点通过alternate互相指向。提交完成后切换角色,这种手法一般叫双缓冲。
这套模型解释得了一个现象:为什么渲染能被用户输入打断。但它毕竟是社区梳理,字段名与流程都会随版本变。
那张「5ms 时间片、优先级车道」的说法同样属于这一层,多见于第三方分析与旧分享。要引数字就一并写出处与年代。
第三层:教学实现的代码
下面这段来自教程式的重实现(Build your own React 那一类),不是 React 源码。它的价值是把「按位往下走、走不动就返回」这件事写清楚了:
// 处理一个工作单元,返回下一个要处理的节点function performUnitOfWork(fiber) { const elements = fiber.props.children reconcileChildren(fiber, elements)
if (fiber.child) { return fiber.child }
let next = fiber while (next) { if (next.sibling) { return next.sibling } next = next.parent } return null}reconcileChildren 做的是同一件事的另一半:拿新的元素列表和旧的 Fiber 链表挨个比,决定这个位置是复用、新增还是删除。
function reconcileChildren(wipFiber, elements) { let index = 0 let oldFiber = wipFiber.alternate && wipFiber.alternate.child let prevSibling = null
while (index < elements.length || oldFiber != null) { const element = elements[index] const sameType = oldFiber && element && element.type === oldFiber.type let newFiber = null
if (sameType) { newFiber = { type: oldFiber.type, props: element.props, dom: oldFiber.dom, parent: wipFiber, alternate: oldFiber, effectTag: 'UPDATE', } } if (element && !sameType) { newFiber = createFiber(element, wipFiber) newFiber.effectTag = 'PLACEMENT' } if (oldFiber && !sameType) { oldFiber.effectTag = 'DELETION' }
if (oldFiber) { oldFiber = oldFiber.sibling } if (index === 0) { wipFiber.child = newFiber } else if (element) { prevSibling.sibling = newFiber }
prevSibling = newFiber index++ }}读它的要点有两个:类型相同就复用旧节点,类型不同就标记新增与删除;整段只在内存里算,一行真实 DOM 都不碰。

这张图配合上面的代码看:从父节点往 child 走,走到底再沿 sibling 横着走,横着也走不动就返回父节点继续。
与 Vue 比的时候注意信息密度
Vue 那一侧的对比算法有官方文档、官方博客与源码讲解;React 这一侧没有对应材料。两边能查到的信息不对等,逐点比较很容易变成拿一方的官方说法对另一方的社区解读。本系列不这么做。
这一层值得记住什么
值得记住的是行为:更新可以被打断、提交不会被打断、相同结果不碰 DOM、身份由位置与 key 决定。
不值得背的是函数名、指针名与时间片长度。它们会变,而且官方从未把它们写成承诺。
分享文章
生成精美分享图或复制链接,与更多人分享本文。
继续阅读
换条路线
从其他文章中稳定抽取
最后更新于 ,距今已过 192 天
部分内容可能已过时