React源码深度探索:揭秘渲染更新机制的奥秘
以下内容从 React 的源码层面出发,逐步剖析其渲染与更新机制的核心原理。文章包含关键代码摘录、ASCII 图解与详细说明,力求让你在学习时能够快速抓住要点,并深入理解 React 团队是如何设计高效的更新调度与渲染流程的。
目录
前言:为什么要研究 React 渲染更新机制
在日常开发中,我们使用 React 提供的高层次 API(如 useState
、useEffect
、React Router 等)快速构建应用界面,却很少深入了解其底层实现。随着应用复杂度增长,性能调优与内存问题往往成为瓶颈:
- 为什么大量元素更新时会卡顿?
- 为什么某些场景下无法中断更新?
- Concurrent Mode 到底改进了哪些底层流程?
了解 React 渲染与更新机制,能帮助我们:
- 更精准地定位性能瓶颈:知道协商(Reconciliation)与提交(Commit)的区别,可判断用
useEffect
还是useLayoutEffect
; - 定制高级优化策略:例如根据更新优先级区分“交互更新”(点击、动画)与“非交互更新”(数据轮询);
- 理解并发模式:如何无阻塞地更新界面、如何中断过期任务、如何保持界面稳定。
下面从源代码角度出发,结合代码示例与 ASCII 图解,逐步揭示 React 渲染更新机制的各个环节。
React 核心架构概览
在深入细节之前,我们先回顾 React 的整体架构。核心组件有:虚拟 DOM、Fiber、调度器、更新队列、副作用(Effect)系统。
组件树、虚拟 DOM 与 Fiber
- 组件树(Component Tree)
React 应用由组件树组成,每个组件返回一个 React 元素(React.createElement(type, props, children)
),最终构建成一棵所谓“虚拟 DOM 树”。 虚拟 DOM(Virtual DOM)
React 会将 JSX 转译为ReactElement
对象,如下所示:const element = <div className="foo"><span>你好</span></div>; // 等价于 const element = React.createElement( 'div', { className: 'foo' }, React.createElement('span', null, '你好') );
在更新时,React 会创建新的虚拟 DOM 树,与旧树做差异比对(diff),然后再将最小化的更新映射到真实 DOM。
Fiber 架构
为了解决大型树更新的阻塞问题,React 16 引入了 Fiber。每个虚拟节点对应一个 Fiber 节点,形成一个双向链表(child
、sibling
、return
指针):FiberNode { type, // FunctionComponent、ClassComponent、HostComponent 等 key, pendingProps, // 本次更新时的新 props memoizedProps, // 上次提交时的 props stateNode, // 对应的真实 DOM 节点或 Class 实例 updateQueue, // 对应的 setState 更新队列 child, sibling, return, // 子节点、兄弟节点、父节点指针 effectTag, // 标记此次更新类型(Placement、Update、Deletion) nextEffect, // 副作用链表指针 // …… 其他字段,例如优先级 lanes、flags 等 }
每次触发更新时,React 都会通过
scheduleUpdateOnFiber(rootFiber)
将根 Fiber 标记为需要更新,然后进入协调(Reconciliation)与提交阶段。
调度器与任务优先级
React 并非简单地“深度优先遍历整棵树再一股脑更新”,而是通过一个调度器(Scheduler)将更新任务拆分成多个可中断的小任务(Fiber Units),并根据优先级动态安排执行。调度器中的核心概念是:
- Sync(同步更新):优先级最高,例如
setState
在事件处理器中直接调用,新内容要马上渲染。 - Discrete(离散事件):如点击、输入等用户交互,可打断闲置任务。
- Continuous(连续事件):如滚动、拖拽,此类任务优先级次之。
- Idle(空闲优先级):低优先级任务,例如日志记录、统计数据上报。
在 React 18+ 中,这一套优先级体系通过 lanes
(多条优先级管道)与 Scheduler
模块协同实现,能够在单次更新中动态调整优先级、抢占当前任务、分片渲染,保证体验流畅。
Reconciliation(协调)阶段深度解析
“协调”指的是 React 将新的虚拟 DOM 树(Fiber 树)与旧的 Fiber 树对比,产生更新标记(effectTag
),并构建一条副作用链表(Effect List)。这一阶段可以被中断,并在下一空闲时段恢复。
旧的 Stack reconciler vs 新的 Fiber reconciler
Stack reconciler(React 15 及以前)
- 同步深度优先遍历节点,直到完成整棵树的遍历后才进行 DOM 更新。
- 大树更新会导致主线程长时间阻塞,用户无法交互。
Fiber reconciler(React 16 以后)
- 引入 Fiber 数据结构,将遍历过程拆分成“工作单元”(work unit),可以被中断、优先级抢占。
- 每次只执行一定量的工作单元,然后让出控制权给浏览器,保证高优先级任务(如用户输入)能够及时响应。
Fiber 节点结构:关键字段与用途
下面是简化版的 Fiber 节点结构,用于说明核心字段含义:
type FiberNode = {
// 标识
tag: WorkTag, // 功能标签:FunctionComponent、HostComponent、ClassComponent 等
key: null | string,
elementType: any, // ReactElement.type
type: any, // Component Function 或 原生标签('div'、'span' 等)
// 更新相关
pendingProps: any, // 本次更新的新 props
memoizedProps: any, // 上一次提交后的 props
memoizedState: any, // 上一次提交后的 state
updateQueue: UpdateQueue, // 链表风格的 setState 更新队列
// 树结构
return: FiberNode | null, // 父节点
child: FiberNode | null, // 第一个子节点
sibling: FiberNode | null,// 下一个兄弟节点
index: number, // 在父节点子链表中的索引
// 真实节点引用
stateNode: any, // 对应真实 DOM 节点(HostComponent)或 Class 实例
// 优先级与调度
lanes: Lanes, // 当前更新所属优先级车道
childLanes: Lanes, // 子树中未完成的更新优先级
// 副作用(Effect)相关
flags: Flags, // 标记本 Fiber 需要做的副作用类型(Placement、Update、Deletion)
subtreeFlags: Flags, // 标记子树中需要收集进入副作用链表的标记
nextEffect: FiberNode | null, // 构建的副作用链表指针
}
pendingProps
vsmemoizedProps
pendingProps
:当前要渲染的新属性(例如setState
后传入的新props
)。memoizedProps
:上一次提交时的属性,用于和pendingProps
做对比,决定是否需要更新。
updateQueue
- 链表风格的更新队列,存放通过
useState
、setState
等方式入队的更新对象(Update
),每次协调时会将队列中所有更新依次应用到上一次的memoizedState
,计算最新memoizedState
。
- 链表风格的更新队列,存放通过
flags
、subtreeFlags
、nextEffect
- 在协调过程中,如果某个 Fiber 发生变化(插入、删除、更新属性等),会在该节点的
flags
标记相应的副作用类型(Placement、Update、Deletion)。 - 同时,这些标记会在向上归的过程中累积到
subtreeFlags
,用于告诉父 Fiber:“我的子树中有需要执行的副作用”。 - 最终会依据
flags
构建一条链表:从根 Fiber 的firstEffect
开始,按执行顺序串联所有需要在提交阶段执行的 Fiber,通过nextEffect
进行遍历。
- 在协调过程中,如果某个 Fiber 发生变化(插入、删除、更新属性等),会在该节点的
UpdateQueue 与更新队列的合并逻辑
当你在函数组件或类组件中多次调用 setState
或 dispatch
,相应的更新并不是立刻执行,而是被收集到当前 Fiber 的更新队列(updateQueue
)中。典型的 updateQueue
结构如下(以类组件为例):
type Update<State> = {
action: any, // setState 中传入的部分 state 或更新函数
priority: Lanes, // 更新优先级
next: Update<State> | null, // 环形链表指针
}
type UpdateQueue<State> = {
baseState: State, // 本次更新队列应用前的 state 基础
firstUpdate: Update<State> | null,
lastUpdate: Update<State> | null,
shared: {
pending: Update<State> | null, // 待处理的更新环形链
}
}
入队逻辑:
- 当调用
setState(updater)
时,React 会创建一个Update
对象,将其插入到shared.pending
环形链表末尾。 - 如果之前已有未处理的
Update
,则newUpdate.next = firstPendingUpdate
,并更新lastPendingUpdate.next = newUpdate
。
- 当调用
消费队列:
在协调(
beginWork
)阶段,React 会从updateQueue.shared.pending
中取出所有Update
,循环应用到baseState
:let resultState = queue.baseState; let update = queue.shared.pending.next; // 第一个更新 do { const action = update.action; resultState = typeof action === 'function' ? action(resultState, props) : { ...resultState, ...action }; update = update.next; } while (update !== null && update !== queue.shared.pending.next);
- 处理完后,将
queue.baseState
更新为新 state,将queue.shared.pending
置空(或保留未处理更新,用于下一轮调度)。 memoizedState
赋值为resultState
,以供后续渲染。
Diff 算法核心流程
在 Fiber reconciler 中,主要分为两种分支:首屏渲染(mount) 与 更新(update)。
挂载(mount)阶段
- 对每个新 Fiber(即 ReactElement 转换来的 Fiber 节点),标记
Placement
,将其插入到真实 DOM(HostComponent)中。 - 不需要比较旧节点,因为旧节点为
null
,所有节点都直接视为“新节点”。
- 对每个新 Fiber(即 ReactElement 转换来的 Fiber 节点),标记
更新(update)阶段
从根 Fiber 开始,深度优先遍历子树:
- 如果 Fiber 对应的
type
(组件类型)相同,执行“更新 props”逻辑,为flags
标记Update
; - 如果不同,则执行“删除旧节点、插入新节点”逻辑,标记
Deletion
与Placement
。
- 如果 Fiber 对应的
对于子节点数组,React 会进行 同级比较:
- 先处理头部与尾部的简单匹配;若都匹配不上,则构建一个键值映射(key →旧 Fiber),用来在 O(n) 时间内找到可复用节点。
- 多余旧节点会被标记为
Deletion
;多余新节点标记为Placement
。
下面是简化的 Diff 过程伪码(匹配多子节点时):
function reconcileChildrenArray(parentFiber, oldChildren, newChildren) { let lastPlacedIndex = 0; let oldFiberMap = mapRemainingChildren(oldChildren); // key → oldFiber for (let i = 0; i < newChildren.length; i++) { const newChild = newChildren[i]; const matchedOldFiber = oldFiberMap.get(newChild.key || i) || null; if (matchedOldFiber) { // 可复用 const newFiber = createWorkInProgress(matchedOldFiber, newChild.props); newFiber.index = i; newFiber.return = parentFiber; // 判断是否需要移动 if (matchedOldFiber.index < lastPlacedIndex) { newFiber.flags |= Placement; // 需要移动 } else { lastPlacedIndex = matchedOldFiber.index; } placeChild(newFiber); oldFiberMap.delete(newChild.key || i); } else { // 新插入 const newFiber = createFiberFromElement(newChild); newFiber.index = i; newFiber.return = parentFiber; newFiber.flags |= Placement; placeChild(newFiber); } } // 剩余 oldFiberMap 中的节点,需要删除 oldFiberMap.forEach((fiber) => { fiber.flags |= Deletion; }); }
关键是:通过键值映射(
oldFiberMap
)加速跨位置节点复用,并通过lastPlacedIndex
标记来判断是否需要插入或移动。
Commit(提交)阶段深度解析
当协调阶段完成,Fiber 树已经标记好了各节点的 flags
(Placement、Update、Deletion 等),并构建出一条按执行顺序排列的“副作用链表”(Effect List)。接下来就是Commit 阶段,分为三个子阶段依次执行:
Before Mutation(突变前)
- 在此阶段,React 会调用所有需要执行
getSnapshotBeforeUpdate
的类组件生命周期,并执行 “DOM 读取” 操作(例如测量位置)。 - 此时 DOM 仍旧是旧版本,不能写入变更。
- 在此阶段,React 会调用所有需要执行
Mutation(突变)
- 真正对 DOM(或原生视图)进行更新:插入新节点、删除旧节点、更新属性、事件注册等。
- 此阶段会执行所有
flags
标记为Placement
、Update
、Deletion
的 Fiber 节点对应的副作用函数(commitHook)。
Layout(布局)
- 在 DOM 发生变更后,调用所有
useLayoutEffect
Hook 与componentDidUpdate
生命周期函数,可在此时安全地读取最新布局并触发后续操作。 - 结束后进入下一轮空闲等待。
- 在 DOM 发生变更后,调用所有
下面用伪代码演示 Commit 阶段的高层逻辑:
function commitRoot(root) {
const firstEffectFiber = root.current.firstEffect;
// Before Mutation 阶段
nextEffect = firstEffectFiber;
while (nextEffect !== null) {
if (nextEffect.flags & Snapshot) {
commitBeforeMutationLifeCycles(nextEffect);
}
nextEffect = nextEffect.nextEffect;
}
// Mutation 阶段
nextEffect = firstEffectFiber;
while (nextEffect !== null) {
const flags = nextEffect.flags;
if (flags & Placement) {
commitPlacement(nextEffect);
}
if (flags & Update) {
commitUpdate(nextEffect);
}
if (flags & Deletion) {
commitDeletion(nextEffect);
}
nextEffect = nextEffect.nextEffect;
}
// 将 root.current 更新为 workInProgress Fiber
root.current = root.finishedWork;
// Layout 阶段
nextEffect = firstEffectFiber;
while (nextEffect !== null) {
if (nextEffect.flags & Layout) {
commitLayoutEffects(nextEffect);
}
nextEffect = nextEffect.nextEffect;
}
}
commitBeforeMutationLifeCycles
:调用getSnapshotBeforeUpdate
、useLayoutEffect
的布局读取逻辑。commitPlacement
:将当前 Fiber 对应的 DOM 节点插入到父节点中(parentNode.insertBefore(dom, sibling)
)。commitUpdate
:更新属性或事件绑定。commitDeletion
:删除节点前先卸载子树生命周期,再从父节点中移除对应 DOM。commitLayoutEffects
:执行useLayoutEffect
回调与componentDidUpdate
生命周期。
三阶段分离保证了:在 Mutation 阶段不做任何 DOM 读取,只关心写入;在 Layout 阶段集中处理所有影响布局的副作用,尽量减少重排(Reflow)次数。
调度与优先级:如何保证流畅体验
协调和渲染的异步分片——work loop
Fiber 核心设计之一就是能在执行协调和提交时 “中途让出”,让浏览器去处理高优先级任务(如用户点击、动画帧)。这一机制由 work loop
负责驱动:
function performUnitOfWork(fiber) {
// 1. beginWork 阶段:对 fiber 及其子节点进行协调(diff)
const next = beginWork(fiber, renderLanes);
if (next !== null) {
return next;
}
// 2. 如果没有子节点可协商,则向上归(completeWork)处理完当前节点
let completed = completeUnitOfWork(fiber);
return completed;
}
function workLoopSync() {
while (nextUnitOfWork !== null) {
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
}
}
function workLoopConcurrent(deadline) {
// deadline 用于判断当前帧时间是否耗尽
while (nextUnitOfWork !== null && !deadline.timeRemaining() < threshold) {
nextUnitOfWork = performUnitOfWork(nextUnitOfTask);
}
// 如果任务尚未完成,安排下一次空闲回调
if (nextUnitOfWork !== null) {
scheduleCallback(workLoopConcurrent);
} else {
// 已完成:执行 commit 阶段
commitRoot(root);
}
}
performUnitOfWork
:执行单个 Fiber 节点的协调或归(begin/complete)逻辑,返回下一个待处理的单元。- 同步模式(
workLoopSync
):直接循环执行所有 Fiber 单元,一鼓作气完成更新。用于优先级最高的更新(同步更新)。 - 并发模式(
workLoopConcurrent
):每次循环会检查deadline.timeRemaining()
,控制在剩余帧时间(通常 \~5ms)内尽量做更多工作,时间耗尽则“让出”给主线程,待下一空闲时间再续做剩余单元。
优先级队列与 lane 概念
在 React 18 中,调度器将多个更新分配到不同的 lane(优先级车道) 中,例如:
- 同步车道(Sync Lane):优先级最高,立即执行,如事件处理函数中的
setState
。 - 离散车道(Discrete Lane):如点击、输入、submit 等离散事件。
- 连续车道(Continuous Lane):如滚动、动画可以被中断的任务。
- 空闲车道(Idle Lane):低优先级,如日志上报。
每个更新会携带一个 lane
标记,进入 Scheduler 时会根据当前已有的任务与其优先级决定是否立即切换工作模式(from Sync → Concurrent → Idle),以及分片时间分配。
type Lanes = number; // 位掩码,表示一组优先级车道
function requestUpdateLane() {
// 正在 ReactEventHandler 中:DiscreteLane
// 正在定时器回调中:ContinuousLane
// ...
return selectedLane;
}
function markRootUpdated(root, lane) {
root.pendingLanes |= lane;
scheduleWorkOnRoot(root, lane);
}
function scheduleWorkOnRoot(root, lane) {
const existingCallback = root.callbackNode;
const newPriority = getHighestPriorityLane(root.pendingLanes);
if (existingCallback !== null) {
const existingPriority = root.callbackPriority;
if (existingPriority === newPriority) {
return;
}
// 如果优先级更高,则取消旧回调
if (existingPriority > newPriority) {
cancelCallback(existingCallback);
}
}
// 根据 newPriority 调度相应回调:同步或并发
if (newPriority === Sync) {
root.callbackNode = scheduleSyncCallback(performSyncWorkOnRoot.bind(null, root));
root.callbackPriority = Sync;
} else {
const schedulerPriority = laneToSchedulerPriority(newPriority);
root.callbackNode = scheduleCallback(schedulerPriority, performConcurrentWorkOnRoot.bind(null, root));
root.callbackPriority = newPriority;
}
}
requestUpdateLane()
:根据当前上下文(事件类型、是否正在 render 阶段)分配合适的lane
。markRootUpdated()
:将更新的lane
标记到根 Fiber 的root.pendingLanes
中,并调用scheduleWorkOnRoot
。scheduleWorkOnRoot()
:比较新旧优先级,决定是立即执行同步更新还是使用并发调度。
这种多车道调度策略使得:
- 用户点击输入这类对响应时间要求高的更新,能被优先调度并同步完成;
- 后台数据轮询、动画渐变等对时延要求不高的更新,会被分片处理,避免阻塞主线程。
Concurrent Mode(并发模式)关键改进
React 16–17 下的 Fiber 已能部分分片渲染,但在 18+ 中,**并发模式(Concurrent Mode)**进一步开放更多接口,支持更细粒度的渲染中断和恢复。
时间切片(Time Slicing)原理
并发模式会在调度更新时始终使用 workLoopConcurrent
,让出控制权更频繁,避免长时间占用主线程。借助浏览器提供的 requestIdleCallback
或自定义的打包版实现,React 能在每个帧的空闲时间片内只执行一小段协调,再让出控制权,举例如下:
帧 0 开始: ┌─────────────┐
主线程空闲中 → 执行 2ms 协调 │ React │ apply Fiber units
→ 用时耗完 → 主线程被还给浏览器 │ workLoop │ (2ms)
↓ └─────────────┘
浏览器渲染帧 0 视觉更新 ▲
→ 16ms 帧时间 │
↓ ┌─────────────┐
主线程空闲 → 执行 2ms 协调 │ React │ apply Fiber units
→ 用时耗完 → 主线程被还给浏览器 │ workLoop │ (2ms)
↓ └─────────────┘
浏览器渲染帧 1
每帧只执行若干毫秒的协调,再让浏览器负责 DOM 提交与重绘,确保卡顿最小化。若在中途收到了高优先级任务(如鼠标点击事件),React 会中断当前调度,优先执行高优先级更新。
中断与恢复机制
在并发模式下,React 通过 shouldYieldToHost()
判断当前帧剩余时间,若不足则将当前 Fiber 节点(nextUnitOfWork
)存储到全局状态,调用 scheduleCallback
继续后续工作。这样可以随时中断,保证用户交互优先。核心逻辑:
function performConcurrentWorkOnRoot(root, didTimeout) {
nextUnitOfWork = createWorkInProgress(root.current, null); // 创建 workInProgress Fiber
workLoopConcurrent(); // 执行并发工作循环
if (nextUnitOfWork !== null) {
// 当前帧未完成,继续调度
root.callbackNode = scheduleCallback(performConcurrentWorkOnRoot.bind(null, root));
} else {
// 全部 Fiber 处理完毕,进入 commit 阶段
commitRoot(root);
}
}
function shouldYieldToHost() {
// 通过 Scheduler API 判断是否已达帧时间阈值
const timeRemaining = getCurrentTime() - frameStartTime;
return timeRemaining >= frameDeadline; // 若超过阈值,则返回 true
}
假设 frameDeadline = 5ms
,每次 performUnitOfWork
消耗 1ms,React 会在执行 5 次单元后让出控制。若遇到高优先级更新(如点击),则会在下一帧立即响应。
源码示例:追踪一次 setState 到更新的完整流程
下面以一个简化的类组件为例,手动跟踪从调用 this.setState
到 DOM 更新的整个流程:
// 简化示例:Counter.js
import React, { Component } from 'react';
import { View, Text, Button } from 'react-native';
export default class Counter extends Component {
constructor(props) {
super(props);
this.state = { count: 0 };
}
increment() {
// ========== 1. 业务组件调用 setState ==========
this.setState({ count: this.state.count + 1 });
}
componentDidUpdate(prevProps, prevState) {
console.log('componentDidUpdate:', prevState.count, '->', this.state.count);
}
render() {
return (
<View>
<Text>Count: {this.state.count}</Text>
<Button title="增加" onPress={() => this.increment()} />
</View>
);
}
}
8.1 组件调用 setState 的上报
- React 在实例化该组件时生成一个对应的 Fiber(设为
fiber
),fiber.stateNode
就是该Counter
Class 实例。 当用户点击“增加”按钮,
onPress
调用this.increment()
,执行setState
:// ReactClassComponent.js 中的 setState 调用 public setState(partialState) { // this._reactInternals 就是该组件对应的 Fiber const fiber = this._reactInternals; // 1. 创建一个 Update 对象 const update = createUpdate(SyncLane); update.payload = partialState; // 2. 将 Update 加入到 fiber.updateQueue enqueueUpdate(fiber, update); // 3. 调度根节点更新 scheduleUpdateOnFiber(fiber, SyncLane, NoTimestamp); }
8.2 生成更新对象并入队
createUpdate(lane)
:生成一个包含payload={ count: oldCount+1 }
的更新对象,优先级设为SyncLane
(同步)。enqueueUpdate(fiber, update)
:将该更新插入到fiber.updateQueue.shared.pending
的环形链表末尾。scheduleUpdateOnFiber(fiber, SyncLane, NoTimestamp)
:开始调度,从当前组件的 Fiber 一直向上找到根 Fiber(fiber.tag === HostRoot
),标记root.pendingLanes |= SyncLane
并调用scheduleWorkOnRoot(root, SyncLane)
。
8.3 调度更新并执行协调
假设当前没有其他更高优先级任务,React 会认为这是一个同步更新,直接进入 workLoopSync
:
function performSyncWorkOnRoot(root) {
// 1. 创建新的 workInProgress Fiber(双缓存:current 与 workInProgress)
workInProgress = createWorkInProgress(root.current, null);
// 2. 开始协调
workLoopSync();
// 3. 协调完成,得到新的 Fiber 树 root.finishedWork
const finishedWork = root.finishedWork;
// 4. 进入提交阶段,将 root.current 指向 finishedWork
commitRoot(root);
}
8.3.1 beginWork:进入 Counter 组件
- 在
workLoopSync
中,调用performUnitOfWork(nextUnitOfWork)
,第一次 nextUnitOfWork 是根 Fiber。最终会遍历到 Counter 组件对应的 Fiber(FunctionComponent or ClassComponent)。 beginWork(fiber, SyncLane)
:对于 ClassComponent,会执行updateClassComponent(fiber, SyncLane, renderExpirationTime)
,包括以下流程:- 计算新的 state:从旧的
fiber.memoizedState
与fiber.updateQueue
中消费所有同步更新,得到新的memoizedState
(count+1
)。 - 执行
render()
:调用fiber.stateNode.render()
渲染新虚拟 DOM。 - 构建新子 Fiber:基于
render()
返回的新 ReactElement 树,与旧的子 Fiber 树调用reconcileChildren
生成新的子 Fiber,并标记placement/update/deletion
。
- 计算新的 state:从旧的
8.4 提交阶段 DOM 更新
经过整个子树的协商后,React 得到一条副作用链(Effect List),记录了“哪些节点需要插入、删除、更新”。此时执行 commitRoot(root)
:
- Before Mutation:调用
getSnapshotBeforeUpdate
(若有)。 Mutation:
- 对 Counter 组件对应的 DOM 节点(Text)进行更新,因为
memoizedProps
与pendingProps
不同,标记Update
,在commitUpdate(fiber)
中执行textNode.textContent = newText
。 - 如果 Counter 有新增子节点或子节点删除,也会在此阶段同步到真实 DOM。
- 对 Counter 组件对应的 DOM 节点(Text)进行更新,因为
- Layout:调用
componentDidUpdate
(Counter 中的日志输出)。 - 最终将
root.current = root.finishedWork
,完成一次更新。
流程结束后,页面上会立刻看到 Count: 1
,且在控制台打印 componentDidUpdate: 0 -> 1
。
图解:Fiber 树与更新链路示意
下面用 ASCII 图展示一个简化的场景:初始渲染与一次 setState
更新的 Fiber 树演变过程。
9.1 初始渲染时的 Fiber 树
假设 App 渲染结构如下:
<App>
<Counter />
</App>
对应的 Fiber 树(简化版):
HostRootFiber
└─ Fiber(AppComponent)
└─ Fiber(CounterComponent)
└─ Fiber(ViewHost) // RN 原生 View
└─ Fiber(TextHost) // 显示 “Count: 0”
└─ Fiber(ButtonHost)
- 每个 Fiber 除了
type
、props
外,memoizedState
初始为{ count: 0 }
(Counter 组件的 state)。 - 所有
flags
均为 0,因为是首次挂载,实际会在 Mount 阶段给对应宿主节点打上Placement
标记。
9.2 调用 setState 后的更新流
- 在 Counter 组件实例里调用
setState({ count: 1 })
,生成一个Update
,插入 CounterFiber 的 UpdateQueue。 - 调度到根 Fiber,进入同步工作循环。
在 CounterFiber 的
beginWork
阶段,React 会从 UpdateQueue 中消费更新:- 旧
memoizedState = { count: 0 }
- 应用 update 的 payload
{ count: 1 }
→ 得到新memoizedState = { count: 1 }
- 旧
- CounterFiber 执行
render()
,返回新的子树(新的<View><Text>Count: 1</Text>...</View>
)。React 将对比新旧子树,发现<Text>
的文本内容变了,标记其 Fiber 的flags = Update
。 - 归的过程中,Fiber 树的
flags
分别累积到父节点的subtreeFlags
。 - 完成协调后,进入提交阶段:找到标记了
Update
的 TextFiber,执行commitUpdate
:textNode.textContent = "Count: 1"
。
更新后 Fiber 树的核心字段变化如下(只展示 Counter 相关):
Fiber(CounterComponent)
├─ memoizedState: { count: 1 }
├─ child: Fiber(ViewHost)
│ └─ child: Fiber(TextHost)
│ ├─ memoizedProps: { children: "Count: 1" }
│ ├─ flags: Update
│ └─ sibling: Fiber(ButtonHost) // Button 不需要更新,flags: 0
最终真实 DOM 更新完成,用户界面从 “Count: 0” 更新到 “Count: 1”。
常见误区与优化建议
误区:所有 setState 都会同步执行
- 其实 React 18+ 在事件回调中触发的
setState
是同步优先级(Sync Lane),会立即执行更新。但在异步回调(例如setTimeout
、XHR 回调)中触发的setState
会被分配到不同优先级,可以并发执行。
- 其实 React 18+ 在事件回调中触发的
误区:Updating setState 会立即更新 DOM
- React 在同步更新时确实会尽快进行协调与提交,但在并发模式(
startTransition
)下,更新可能被延后,以免阻塞更高优先级的渲染。
- React 在同步更新时确实会尽快进行协调与提交,但在并发模式(
优先使用
useState
与useReducer
而非手动修改 Context- 对于共享状态,如果仅用 Context 而不配合
useReducer
,每次修改都会导致全部订阅组件重渲染,性能难以控制。
- 对于共享状态,如果仅用 Context 而不配合
避免在 render 中做大计算
- render 阶段是可中断的,但过于耗时的计算会增加各个 Fiber 的执行时间,导致中断点变少,影响并发体验。可将耗时逻辑放到
useMemo
、useEffect
或后端处理。
- render 阶段是可中断的,但过于耗时的计算会增加各个 Fiber 的执行时间,导致中断点变少,影响并发体验。可将耗时逻辑放到
合理拆分组件
- 过大的组件会导致单一 Fiber 包含大量子孙节点,更新时一次性需遍历的节点过多,不利于中断调度。可考虑拆分成更小的子组件。
避免在
useLayoutEffect
中做大量 DOM 操作useLayoutEffect
会在 Mutation 阶段后立即执行,有可能导致布局抖动,影响渲染流畅。仅在必要时使用。
结语:如何进一步钻研 React 源码
本文详细剖析了 React 渲染更新机制的各个关键环节:
- Fiber 数据结构与协调(Reconciliation)
- Commit 阶段的三次子阶段划分
- 调度器、多优先级 lanes 与并发时间切片
- 整个更新流程的代码示例与图解追踪
要进一步深入,可以从以下几个方向继续探索:
深入 Scheduler 调度器
- 阅读
scheduler
包 源码,理解requestIdleCallback
、shouldYieldToHost
、四种优先级如何转换到 browser callback。
- 阅读
研究 Hooks 源码
useState
、useEffect
、useReducer
在 Fiber 内部是如何注册与销毁的,关注mountHook
、updateHook
等实现细节。
并发特性
- 在 React 18+ 中启用
createRoot
并体验并发模式,阅读ReactFiberConcurrentUpdates.js
、ReactFiberWorkLoop.js
等文件,体会新增的startTransition
、useDeferredValue
等 Hook 如何与调度器协作。
- 在 React 18+ 中启用
内存泄漏与回收
- 了解 React 如何回收被删节点的 Fiber,如
completeDeletion
、clearContainer
的实现,以及与 JS 垃圾回收的关系。
- 了解 React 如何回收被删节点的 Fiber,如
源码调试技巧
- 在
[...]/packages/react-reconciler/src/ReactFiberWorkLoop.new.js
等文件设置断点,结合 DevTools 观察 Fiber 节点状态变化。
- 在
希望本文能帮助你搭建学习 React 源码的“第一座桥”,并为性能优化与调度改进提供有力支撑。继续深入研究,吸收更多底层原理,你将能更加自如地运用 React,创造出既易维护又高性能的前端应用。
评论已关闭