深色模式
React 16 在 2017 年 9 月发布,底层做了彻底重写,核心就是 Fiber 架构。这次重写不是为了性能数字好看,而是为了解决 React 15 在复杂应用里的根本性架构问题。
React 15 的问题:同步不可中断的渲染
React 15 的渲染过程是同步的、递归的:一旦开始渲染(调用 setState),就必须一口气走完整个组件树的 diff 和更新,期间无法暂停。
浏览器的主线程同时负责 JS 执行、页面渲染、响应用户输入。如果 React 的渲染占用主线程超过 16ms(60fps 的单帧预算),下一帧的渲染就会推迟,用户会感知到卡顿。
对于组件树规模不大的应用,16ms 通常够用。但当树很深或更新频繁时,这个同步阻塞就成了瓶颈。
Fiber 的核心思想:把渲染拆成可中断的工作单元
Fiber 这个名字来自操作系统里的"纤程"概念——比线程更细粒度的工作单元。Fiber 架构把渲染过程拆分成两个阶段:
阶段一:Reconciliation(协调/可中断)
- 遍历组件树,计算需要哪些变更(diff)
- 这个阶段可以被暂停、恢复、甚至丢弃
- 不会修改真实 DOM
阶段二:Commit(提交/同步不可中断)
- 把计算好的变更批量应用到真实 DOM
- 必须同步完成,确保 UI 一致性
阶段一可以利用浏览器空闲时间分批执行:
帧1: [React渲染部分] → [浏览器绘制] → [React继续渲染] ...
帧2: [React渲染剩余] → [浏览器绘制] → [提交DOM变更]这样 React 不再独占主线程,高优先级任务(用户输入、动画)可以插队。
Fiber 节点结构
每个 React 组件对应一个 Fiber 节点,Fiber 把原来的递归调用栈"平铺"成了一个链表:
虚拟 DOM 树: Fiber 链表结构:
App App
/ \ ↓ child
Foo Bar → Foo → Bar
| ↓ child
Baz Baz (sibling → Bar, return → Foo)每个 Fiber 节点包含:
type:组件类型stateNode:对应的 DOM 节点或组件实例child:第一个子 Fibersibling:下一个兄弟 Fiberreturn:父 FiberpendingProps/memoizedProps:新旧 propseffectTag:需要执行的 DOM 操作(Insert、Update、Delete)
用链表代替递归调用栈,使得"保存当前进度,下次继续"成为可能。
优先级调度
Fiber 引入了任务优先级的概念:
javascript
// React 内部的优先级层级(简化)
const priorities = {
Synchronous: 1, // 同步,比如 setState 在事件处理里
Task: 2, // 当前 tick 内
Animation: 3, // 下一帧前
High: 4, // 快,但不是立刻
Low: 5, // 可以等
Offscreen: 6, // 隐藏内容,最低优先级
};高优先级更新(如用户输入触发的状态变更)可以中断低优先级更新(如数据请求后的列表渲染)。
对开发者的影响:生命周期变化
Fiber 的分阶段渲染有个副作用:阶段一(协调阶段)可能被执行多次(因为可以被中断重来)。这意味着某些生命周期函数可能被调用多次:
协调阶段(可能多次调用):
componentWillMountcomponentWillReceivePropscomponentWillUpdateshouldComponentUpdaterender
提交阶段(只调用一次):
componentDidMountcomponentDidUpdatecomponentWillUnmount
正因为如此,React 16 开始不推荐在协调阶段的生命周期里做副作用(API 调用、手动 DOM 操作)。React 在 16.3 会给这些生命周期加上 UNSAFE_ 前缀作为警告,最终在 17+ 版本废弃。
现在能用上 Fiber 的哪些特性
React 16.0 的 Fiber 调度能力还没有完全开放,同步优先级之外的异步调度还在开发中。已经可以使用的是:
- Error Boundaries:
componentDidCatch createPortal:把子组件渲染到任意 DOM 节点(做 Modal 很方便)render返回数组和字符串:不再强制要求单根元素
javascript
// render 可以返回数组,不需要多余的包裹 div
render() {
return [
<li key="1">Item 1</li>,
<li key="2">Item 2</li>,
<li key="3">Item 3</li>
]
}
// createPortal:把 Modal 渲染到 body 上,避免 z-index 和 overflow 问题
import { createPortal } from 'react-dom'
class Modal extends React.Component {
render() {
return createPortal(
<div className="modal">{this.props.children}</div>,
document.body
)
}
}Fiber 架构的完整潜力(Concurrent Mode、Suspense)会在后续版本逐步释放。
下一篇:Babel 7 升级迁移实战