1157 字
6 分钟
React 的 Fiber 架构

先分清三层来源#

这一篇讲的是 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 都不碰。

Fiber 树的遍历与双缓冲示意

这张图配合上面的代码看:从父节点往 child 走,走到底再沿 sibling 横着走,横着也走不动就返回父节点继续。

与 Vue 比的时候注意信息密度#

Vue 那一侧的对比算法有官方文档、官方博客与源码讲解;React 这一侧没有对应材料。两边能查到的信息不对等,逐点比较很容易变成拿一方的官方说法对另一方的社区解读。本系列不这么做。

这一层值得记住什么#

值得记住的是行为:更新可以被打断、提交不会被打断、相同结果不碰 DOM、身份由位置与 key 决定。

不值得背的是函数名、指针名与时间片长度。它们会变,而且官方从未把它们写成承诺。

第 37 篇,共 39 篇本文属于系列

React 的 Fiber 架构
https://www.choria.top/posts/react-fiber/
作者
乔瑞雅·克林威尔
发布于
2026-04-30
许可协议
CC BY-NC-SA 4.0

分享文章

生成精美分享图或复制链接,与更多人分享本文。

继续阅读

沿着主题读

基于共同的标签与分类

换条路线

从其他文章中稳定抽取

最后更新于 ,距今已过 192 天

部分内容可能已过时