Skip to content

React Fiber 架构与调和算法

React 16 引入 Fiber 架构,彻底重写了调和(Reconciliation)引擎,实现了可中断渲染和并发模式。

为什么需要 Fiber

React 15 的 Stack Reconciler 是递归同步的,一旦开始就无法中断:

Stack Reconciler(React 15):
  renderComponent(App)
    renderComponent(Header)
      renderComponent(Nav)  ← 深度递归,无法暂停
        ...
  
问题:大型组件树更新时,JS 线程被占用超过 16ms(60fps 的帧预算),
导致动画卡顿、用户输入无响应。

Fiber 的解决方案:将渲染工作切分为小单元,可以暂停、恢复、丢弃

Fiber 节点结构

ts
// 每个 React 元素对应一个 Fiber 节点
interface FiberNode {
  // 节点类型
  type: string | Function | null
  key: string | null

  // 链表结构(替代递归调用栈)
  child: FiberNode | null      // 第一个子节点
  sibling: FiberNode | null    // 下一个兄弟节点
  return: FiberNode | null     // 父节点

  // 状态
  pendingProps: any
  memoizedProps: any
  memoizedState: any           // Hook 链表的头节点

  // 副作用
  flags: Flags                 // 需要执行的操作(插入/更新/删除)
  subtreeFlags: Flags
  updateQueue: UpdateQueue     // 待处理的更新队列

  // 双缓冲
  alternate: FiberNode | null  // 对应的另一棵树的节点
}

双缓冲机制

React 维护两棵 Fiber 树:

current tree(当前显示的)    workInProgress tree(正在构建的)
        ┌──────┐                      ┌──────┐
        │ App  │◄──── alternate ─────►│ App  │
        └──┬───┘                      └──┬───┘
           │                             │
        ┌──▼───┐                      ┌──▼───┐
        │Header│◄──── alternate ─────►│Header│
        └──────┘                      └──────┘

渲染完成后,workInProgress 变成新的 current

渲染两个阶段

Render 阶段(可中断)

beginWork(向下):
  App → Header → Nav → Button

completeWork(向上):
  Button → Nav → Header → App

每个节点的 beginWork:
  1. 根据 type 创建/更新 Fiber 节点
  2. 对比新旧 props,标记副作用(flags)
  3. 返回第一个子节点(继续向下)

每个节点的 completeWork:
  1. 创建/更新 DOM 节点
  2. 收集子树的副作用到父节点(subtreeFlags)

Commit 阶段(同步,不可中断)

三个子阶段:
  1. BeforeMutation:调用 getSnapshotBeforeUpdate
  2. Mutation:执行 DOM 操作(插入/更新/删除)
  3. Layout:调用 componentDidMount/Update,useLayoutEffect

Diff 算法

React 的 Diff 基于三个假设(启发式算法,O(n) 复杂度):

  1. 不同类型的元素产生不同的树
  2. 同级元素通过 key 标识
  3. 开发者可以通过 key 提示哪些子元素是稳定的
jsx
// 规则一:类型不同 → 销毁重建
// 旧:<div><Counter /></div>
// 新:<span><Counter /></span>
// Counter 会被卸载再重新挂载(state 丢失)

// 规则二:key 的重要性
// ❌ 没有 key:列表重排时,React 逐个对比,可能错误复用
function BadList({ items }) {
  return items.map(item => <Item value={item.value} />)
}

// ✅ 有 key:React 通过 key 识别节点身份
function GoodList({ items }) {
  return items.map(item => <Item key={item.id} value={item.value} />)
}

// ❌ 不要用 index 作为 key(列表有增删时)
items.map((item, index) => <Item key={index} />)
// 删除第一项后,所有 key 都变了,React 会错误地复用组件

// ✅ 用稳定的唯一 ID
items.map(item => <Item key={item.id} />)

并发特性(React 18)

jsx
import { useTransition, useDeferredValue, startTransition } from 'react'

// useTransition:标记低优先级更新
function SearchPage() {
  const [query, setQuery] = useState('')
  const [results, setResults] = useState([])
  const [isPending, startTransition] = useTransition()

  function handleChange(e) {
    // 高优先级:立即更新输入框
    setQuery(e.target.value)

    // 低优先级:搜索结果可以延迟
    startTransition(() => {
      setResults(search(e.target.value))
    })
  }

  return (
    <>
      <input value={query} onChange={handleChange} />
      {isPending ? <Spinner /> : <Results data={results} />}
    </>
  )
}

// useDeferredValue:延迟某个值的更新
function App() {
  const [text, setText] = useState('')
  const deferredText = useDeferredValue(text)  // 延迟跟随 text

  return (
    <>
      <input value={text} onChange={e => setText(e.target.value)} />
      {/* SlowList 使用 deferredText,不会阻塞输入 */}
      <SlowList text={deferredText} />
    </>
  )
}

Scheduler 调度器

优先级(从高到低):
  ImmediatePriority    → 同步,立即执行(如用户输入)
  UserBlockingPriority → 250ms 内(如点击、hover)
  NormalPriority       → 5s 内(如网络请求回调)
  LowPriority          → 10s 内(如数据预加载)
  IdlePriority         → 空闲时执行(如日志上报)

调度机制:
  - 使用 MessageChannel 实现异步调度(比 setTimeout 更精确)
  - 每帧预留 5ms 给 React 工作
  - 超时则暂停,下一帧继续(时间切片)

总结

  • Fiber 将渲染拆分为可中断的小单元,解决了大树更新卡顿问题
  • 双缓冲:current 树显示,workInProgress 树在后台构建
  • Render 阶段可中断,Commit 阶段同步不可中断
  • Diff 算法是启发式 O(n),key 是正确 Diff 的关键
  • React 18 并发特性(useTransition/useDeferredValue)基于 Fiber 优先级调度

系统学习 Web 前端生态,深入底层架构