mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4
671 字
2 分钟
React 渲染流程:从 setState 到 DOM 更新
2023-04-27

前言#

React 的渲染机制是前端面试的高频考点,也是性能优化的理论基础。本文将深入剖析从 setState 调用到 DOM 更新的完整流程,揭示 React Fiber 架构的精妙设计。

React 渲染流程概览#

flowchart TB A[触发更新] --> B[调度器 Scheduler] B --> C[协调器 Reconciler] C --> D[构建 Fiber 树] D --> E[Diff 算法] E --> F[生成副作用链] F --> G[渲染器 Renderer] G --> H[执行 DOM 操作] subgraph 可中断 C D E end

一、触发更新#

1.1 更新触发方式#

React 中有多种触发更新的方式:

// 类组件
this.setState({ count: this.state.count + 1 });
this.forceUpdate();
// 函数组件
const [count, setCount] = useState(0);
setCount(count + 1);
// Hooks
useReducer, useRef, useContext...

1.2 更新优先级#

React 18 引入优先级调度:

flowchart LR A[用户输入] --> B[同步优先级] C[数据获取] --> D[默认优先级] E[分析日志] --> F[空闲优先级] B --> G[高优先级先执行] D --> H[中优先级] F --> I[低优先级后执行]
优先级类型场景
Immediate同步用户输入、离散事件
UserBlocking用户阻塞悬停、点击
Normal默认网络请求、数据更新
Low低优先级分析日志
Idle空闲预加载、缓存

1.3 更新队列#

每次 setState 创建一个更新对象:

// 更新对象结构(简化)
const update = {
action: { count: 1 }, // 或 updater 函数
lane: DefaultLane, // 优先级
next: null, // 链表指针
};
// 挂载到 Fiber 节点
fiber.updateQueue = {
shared: {
pending: update, // 环形链表
},
};

二、调度器(Scheduler)#

2.1 调度原理#

调度器负责安排更新任务的执行时机:

sequenceDiagram participant U as 更新触发 participant S as Scheduler participant B as 浏览器 participant W as WorkLoop U->>S: scheduleCallback(priority, callback) S->>S: 计算过期时间 S->>B: requestIdleCallback / MessageChannel B-->>S: 空闲时回调 S->>W: 执行工作单元 W-->>S: 是否还有工作? S->>B: 继续请求空闲时间

2.2 时间切片#

React 使用时间切片避免阻塞主线程:

// 每个工作单元的时间限制(5ms)
const YIELD_INTERVAL = 5;
let startTime = -1;
function shouldYield() {
const elapsedTime = getCurrentTime() - startTime;
return elapsedTime >= YIELD_INTERVAL;
}
gantt title React 时间切片 dateFormat X axisFormat %s section 第一帧 Render Phase 1 :0, 5 浏览器渲染 :5, 16 section 第二帧 Render Phase 2 :16, 21 浏览器渲染 :21, 32

2.3 MessageChannel#

React 使用 MessageChannel 实现调度:

const channel = new MessageChannel();
const port = channel.port2;
channel.port1.onmessage = performWorkUntilDeadline;
function schedulePerformWorkUntilDeadline() {
port.postMessage(null);
}
function performWorkUntilDeadline() {
startTime = getCurrentTime();
// 执行工作...
if (hasMoreWork && !shouldYield()) {
schedulePerformWorkUntilDeadline();
}
}

三、Fiber 架构#

3.1 Fiber 节点结构#

每个组件对应一个 Fiber 节点:

function FiberNode(tag, pendingProps, key) {
// 实例属性
this.tag = tag; // 组件类型
this.key = key; // key
this.type = null; // 函数组件/类组件/原生标签
this.stateNode = null; // DOM 节点或组件实例
// Fiber 树结构
this.return = null; // 父节点
this.child = null; // 第一个子节点
this.sibling = null; // 下一个兄弟节点
this.index = 0; // 在父节点中的索引
// Props 和 State
this.pendingProps = pendingProps;
this.memoizedProps = null;
this.updateQueue = null;
this.memoizedState = null;
// 副作用
this.flags = NoFlags; // 副作用标记
this.subtreeFlags = NoFlags;
this.deletions = null;
// 替代树(用于双缓冲)
this.alternate = null;
}

3.2 Fiber 树结构#

// JSX
<div id="app">
<h1>Title</h1>
<p>Paragraph</p>
</div>
// Fiber 树结构
Fiber(div)
/ \
Fiber(h1) Fiber(p)
| |
Fiber(text) Fiber(text)
flowchart TB A[App Fiber] --> B[div Fiber] B --> C[h1 Fiber] B --> D[p Fiber] C --> E[text Fiber] D --> F[text Fiber] A -.->|alternate| A2[App Fiber' workInProgress] B -.->|alternate| B2[div Fiber']

3.3 双缓冲机制#

React 使用双缓冲避免闪烁:

sequenceDiagram participant C as Current Tree participant W as WorkInProgress Tree participant D as DOM Note over C: 当前显示的树 Note over W: 正在构建的树 W->>W: 构建 WorkInProgress 树 W->>W: Diff 计算 W->>W: 收集副作用 Note over W: 构建完成 C-->>W: 交换指针 W->>D: 提交 DOM 更新 Note over C: WorkInProgress 变成 Current

四、协调器(Reconciler)#

4.1 开始协调#

协调器遍历 Fiber 树:

function performUnitOfWork(unitOfWork) {
const current = unitOfWork.alternate;
// 1. 处理当前 Fiber
let next = beginWork(current, unitOfWork, renderLanes);
// 2. 更新 memoizedProps
unitOfWork.memoizedProps = unitOfWork.pendingProps;
// 3. 如果没有子节点,完成当前节点
if (next === null) {
completeUnitOfWork(unitOfWork);
} else {
return next; // 返回子节点继续处理
}
return null; // 返回兄弟节点或回到父节点
}

4.2 遍历顺序#

Fiber 树的深度优先遍历:

flowchart TB A[App] --> B[div] B --> C[h1] C --> D[text: Title] B --> E[p] E --> F[text: Paragraph] style A fill:#f9f style B fill:#bbf style C fill:#bfb style D fill:#fbb style E fill:#bfb style F fill:#fbb
遍历顺序:
beginWork(App) → beginWork(div) → beginWork(h1) → beginWork(text)
→ completeWork(text) → completeWork(h1)
→ beginWork(p) → beginWork(text) → completeWork(text) → completeWork(p)
→ completeWork(div) → completeWork(App)

4.3 beginWork#

处理单个 Fiber 节点:

function beginWork(current, workInProgress, renderLanes) {
switch (workInProgress.tag) {
case FunctionComponent:
return updateFunctionComponent(current, workInProgress, renderLanes);
case ClassComponent:
return updateClassComponent(current, workInProgress, renderLanes);
case HostComponent: // div, span 等
return updateHostComponent(current, workInProgress, renderLanes);
case HostText: // 文本节点
return updateHostText(current, workInProgress);
// ...
}
}

4.4 completeWork#

完成 Fiber 处理,收集副作用:

function completeWork(current, workInProgress, renderLanes) {
const newProps = workInProgress.pendingProps;
switch (workInProgress.tag) {
case HostComponent: {
// 创建或更新 DOM 节点
if (current === null) {
// mount: 创建 DOM
const instance = createInstance(type, newProps);
appendAllChildren(instance, workInProgress);
workInProgress.stateNode = instance;
} else {
// update: 更新属性
workInProgress.flags |= Update;
}
break;
}
case HostText: {
// 文本节点
const newText = newProps;
if (current === null) {
workInProgress.stateNode = createTextInstance(newText);
} else if (current.memoizedProps !== newText) {
workInProgress.flags |= Update;
}
break;
}
}
// 冒泡副作用
bubbleProperties(workInProgress);
}

五、Diff 算法#

5.1 Diff 策略#

React 的 Diff 算法基于三个假设:

  1. 不同类型元素:直接替换
  2. 同类型 DOM 元素:更新属性
  3. 同类型组件元素:更新 props
// 策略 1:不同类型,直接替换
<div><span /></div> → <div><p /></div>
// 删除 span,创建 p
// 策略 2:同类型 DOM,更新属性
<div className="a" /> → <div className="b" />
// 只更新 className
// 策略 3:同类型组件,更新 props
<App name="A" /> → <App name="B" />
// 重新渲染 App

5.2 列表 Diff#

对子元素列表的 Diff 使用 key:

// 旧列表
<ul>
<li key="a">A</li>
<li key="b">B</li>
<li key="c">C</li>
</ul>
// 新列表(插入 d)
<ul>
<li key="a">A</li>
<li key="d">D</li> // 新插入
<li key="b">B</li>
<li key="c">C</li>
</ul>

Diff 过程

flowchart TB A[遍历新列表] --> B{key 匹配?} B -->|匹配| C[复用 Fiber] B -->|不匹配| D[创建新 Fiber] C --> E[移动位置] D --> F[标记 Placement] E --> G{还有剩余?} F --> G G -->|是| A G -->|否| H[处理删除]

5.3 key 的重要性#

// 使用 index 作为 key(不推荐)
{
items.map((item, index) => <Item key={index} item={item} />);
}
// 使用唯一 id 作为 key
{
items.map(item => <Item key={item.id} item={item} />);
}

index 作为 key 的问题

原列表: [a, b, c] key: [0, 1, 2]
插入 d 到开头: [d, a, b, c] key: [0, 1, 2, 3]
Diff 结果:
- key=0: a → d(错误地复用)
- key=1: b → a(错误地复用)
- key=2: c → b(错误地复用)
- key=3: c(新增)
实际应该: 创建 d,移动 a, b, c

六、副作用标记#

6.1 副作用类型#

React 使用二进制标记副作用:

// 副作用标记(部分)
export const NoFlags = /* */ 0b00000000000000000000;
export const PerformedWork = /* */ 0b00000000000000000001;
export const Placement = /* */ 0b00000000000000000010;
export const Update = /* */ 0b00000000000000000100;
export const Deletion = /* */ 0b00000000000000001000;
export const ChildDeletion = /* */ 0b00000000000100000000;

6.2 副作用链#

React 将有副作用的 Fiber 收集为链表:

// 完成协调后,副作用链结构
// 按深度优先顺序连接
Fiber(div, flags=Placement)
└── nextEffect → Fiber(h1, flags=Update)
└── nextEffect → Fiber(p, flags=Deletion)
└── nextEffect → null

七、渲染器(Renderer)#

7.1 提交阶段#

协调完成后进入提交阶段:

function commitRoot(root) {
const finishedWork = root.finishedWork;
// 1. 提交前处理(如 getSnapshotBeforeUpdate)
commitBeforeMutationEffects(finishedWork);
// 2. 提交 DOM 变更
commitMutationEffects(finishedWork, root);
// 3. 提交后处理(如 componentDidMount/Update)
commitLayoutEffects(finishedWork, root);
// 确保后续更新在下一帧
flushSyncCallbacks();
}

7.2 DOM 操作顺序#

flowchart LR A[遍历副作用链] --> B[处理 Deletion] B --> C[处理 Placement] C --> D[处理 Update] subgraph Deletion B1[删除 DOM 节点] B2[调用 componentWillUnmount] end subgraph Placement C1[插入 DOM 节点] C2[调用 componentWillMount] end subgraph Update D1[更新 DOM 属性] D2[调用 componentWillUpdate] end

7.3 生命周期调用时机#

// 挂载阶段
constructor → getDerivedStateFromProps → render →
componentDidMount
// 更新阶段
getDerivedStateFromProps → shouldComponentUpdate → render →
getSnapshotBeforeUpdate → componentDidUpdate
// 卸载阶段
componentWillUnmount

八、并发模式#

8.1 并发渲染#

React 18 的并发特性:

// 启动并发渲染
ReactDOM.createRoot(document.getElementById("root")).render(<App />);
// 使用 startTransition 标记低优先级更新
import { startTransition } from "react";
startTransition(() => {
setSearchResults(results); // 低优先级
});

8.2 优先级抢占#

sequenceDiagram participant H as 高优先级更新 participant L as 低优先级更新 participant R as React L->>R: 开始渲染低优先级 R->>R: 渲染到一半... H->>R: 高优先级更新到达! R->>R: 中断当前渲染 R->>R: 渲染高优先级 R-->>H: 提交高优先级结果 R->>R: 恢复低优先级渲染 R-->>L: 提交低优先级结果

8.3 Suspense#

// Suspense 组件
<Suspense fallback={<Loading />}>
<DataComponent />
</Suspense>;
// DataComponent 内部
function DataComponent() {
const data = use(fetchData()); // 抛出 Promise
return <div>{data}</div>;
}
sequenceDiagram participant R as React participant C as Component participant S as Suspense R->>C: 渲染组件 C->>R: 抛出 Promise R->>S: 显示 fallback Note over R: 等待 Promise resolve R->>C: 重新渲染 R->>S: 显示内容

九、性能优化#

9.1 避免不必要的渲染#

// 使用 React.memo
const MemoComponent = React.memo(function Component({ data }) {
return <div>{data}</div>;
});
// 使用 useMemo 缓存计算
function Component({ items }) {
const sortedItems = useMemo(() => {
return items.sort((a, b) => a - b);
}, [items]);
return <List items={sortedItems} />;
}
// 使用 useCallback 缓存函数
function Parent() {
const handleClick = useCallback(() => {
console.log("clicked");
}, []);
return <Child onClick={handleClick} />;
}

9.2 虚拟列表#

// 使用 react-window 或 react-virtualized
import { FixedSizeList } from "react-window";
function VirtualList({ items }) {
return (
<FixedSizeList height={600} itemCount={items.length} itemSize={35}>
{({ index, style }) => <div style={style}>{items[index]}</div>}
</FixedSizeList>
);
}

9.3 代码分割#

import { lazy, Suspense } from "react";
// 懒加载组件
const HeavyComponent = lazy(() => import("./HeavyComponent"));
function App() {
return (
<Suspense fallback={<Loading />}>
<HeavyComponent />
</Suspense>
);
}

十、调试工具#

10.1 React DevTools#

Components 面板:
├── 查看组件树
├── 查看 Props/State
├── 高亮更新(渲染次数)
└── 追踪渲染原因
Profiler 面板:
├── 录制渲染性能
├── 分析组件渲染时间
└── 识别性能瓶颈

10.2 为什么渲染分析#

// 使用 why-did-you-render
import whyDidYouRender from "@welldone-software/why-did-you-render";
if (process.env.NODE_ENV === "development") {
whyDidYouRender(React, {
trackAllPureComponents: true,
});
}
// 标记组件
Component.whyDidYouRender = true;

总结#

React 渲染完整流程#

flowchart TB subgraph 触发 A[setState/dispatch] end subgraph 调度 B[创建更新对象] C[计算优先级] D[加入调度队列] end subgraph 协调 E[构建 WorkInProgress 树] F[执行 Diff 算法] G[收集副作用] end subgraph 提交 H[提交前副作用] I[执行 DOM 操作] J[提交后副作用] end A --> B --> C --> D --> E --> F --> G --> H --> I --> J

关键要点#

  1. Fiber 架构:可中断的渲染过程
  2. 调度器:优先级调度、时间切片
  3. 协调器:Diff 算法、副作用收集
  4. 渲染器:DOM 操作、生命周期
  5. 并发模式:优先级抢占、Suspense

支持与分享

如果这篇文章对你有帮助,欢迎支持作者或分享给更多人

React 渲染流程:从 setState 到 DOM 更新
https://blog.souloss.com/posts/principles/react-rendering-process/
作者
Souloss
发布于
2023-04-27
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时