面试官最常追问的 React 问题,绕不开两棵「树」:Render 树和 Fiber 树。别被名字吓到------这篇把更新从 setState 到 DOM 落地的完整链路拆开,配上流程图,讲完就能对上号。
读完你会知道:一次状态更新到底经历了什么,为什么页面不卡顿,以及虚拟列表、批量更新这些优化手段怎么和 Fiber 配合。
01Render 树、Fiber 树、DOM 树:三棵「树」各司其职
很多人把三棵树混为一谈,其实它们分工完全不同:
Render 树:描述 UI 的轻量结构,只存类型/props/children,供 diff 算最小更新
Fiber 树:React 16+ 的工作单元,可中断、增量渲染、按优先级调度
DOM 树:浏览器真实渲染的节点,commit 阶段按需更新,操作代价最高
Fiber 节点的结构很关键:type、key、props、stateNode 记录组件信息,child、sibling、return 组成链表(替代了 React 15 的递归树),再通过 effect list 管理副作用,在 commit 阶段统一执行 DOM 更新和 hook 副作用。

图1 三棵树的关系:Render 描述 UI,Fiber 负责调度,DOM 才是真实页面
面试这样说
Render 树描述 UI,DOM 树才是真实页面;Fiber 支持可中断和增量渲染。哪怕大量组件同时更新,也不会每次全量渲染 DOM。
02一次 setState,到底走了哪几步
状态更新的源头有两类:Class 组件的 this.setState() ,以及 Function 组件的 useState setter / useReducer dispatch。触发后的完整链路是:
① 生成 Update 对象,加入该组件的更新队列
② Scheduler 读队列、判优先级,构建 work-in-progress Fiber 树
③ 新旧 Fiber 树 diff,计算最小 DOM 更新
④ Commit 阶段执行 DOM 更新 + 副作用(useEffect / useLayoutEffect)

图2 一次 setState 的完整旅程:入队 → 调度 → WIP 树 → diff → commit
关键认知:React 并非每次 setState 都重建整个 DOM,而是生成新的 WIP Fiber 树,通过 diff 只更新真正变化的节点。
面试这样说
要能讲清 state/props 改变如何进入队列、触发 reconcile,再落到 commit。这是渲染机制的主线。
03更新队列与批量更新:N 次 setState,只渲染一次
每个 Fiber 节点都维护自己的更新队列(update queue),存储待处理更新。好处是:多次 setState 累积合并,减少无效 render;再结合优先级调度,保证关键任务优先执行。
批量更新的版本差异是高频考点:
React 17 及之前:事件处理函数内的 setState 批量;Promise / setTimeout 不批量
React 18+:automatic batching,同步/异步更新都自动批量合并
const count, setCount = useState(0);
const handleClick = () => {
setCount(c => c + 1);
setCount(c => c + 1);
setCount(c => c + 1);
}; // React 18+ 最终只 render 一次,count 增加 3
注意这里必须用函数式更新 (c => c + 1):它会基于上一次的结果累加,而不是覆盖,所以三次 +1 最终变成 +3。

图3 同一优先级的多个 setSta
e 经 Scheduler 合并,最终只 render 一次
** 面试这样说**
批量更新机制结合 Fiber 的增量渲染共同优化性能;能举出「一个组件内部多次 setState 如何合并、避免重复 render」的例子才算过关。
04Scheduler:优先级调度,让页面不卡顿
React 16+ 引入调度器(Scheduler)管理任务优先级,避免主线程被长任务阻塞。优先级分三档:
同步任务:用户点击、输入,优先级最高
过渡任务(transition):页面切换、列表滚动,可中断
空闲任务(idle):非紧急更新,最低优先级
实现上,Fiber 把更新拆成小块(unit of work),用 MessageChannel / RAF 在浏览器空闲时间处理低优先级任务。经典场景:页面同时有表单输入和长列表加载,输入优先渲染,列表渲染可中断------页面因此不卡顿。
05高频场景优化:虚拟列表、批量更新与 RAF
长列表全部渲染会消耗大量内存、拖慢首屏。解法是虚拟列表(react-window / react-virtualized):只渲染可视区域节点,随滚动动态加载/卸载。配合 Fiber,滚动产生的节点更新被拆成小单元逐帧渲染,主线程不阻塞。
高频更新(滚动、拖拽)则用 requestAnimationFrame 与批量更新配合:RAF 在下一次绘制前触发回调,把多次状态更新合并进一个渲染周期。
let pending = false;
function updateState() {
if (!pending) {
pending = true;
requestAnimationFrame(() => {
setCount(c => c + 1);
setCount(c => c + 1);
pending = false;
});
}
};
这样避免高频更新触发大量 render,与 React 批量更新机制配合,减少 commit 次数。
06面试高频追问:快速对答
每次修改都会生成新的 render 树吗?
每次 setState 生成新的 work-in-progress Fiber 树,而非全量 DOM;最终 commit 只做最小化 DOM 更新。
渲染如何得知要更新?
状态或 props 改变会进入更新队列,由 Scheduler 调度生成新 Fiber 树,diff 计算更新。
一个组件里 99 个更新,什么时候批量?
每个 Fiber 有独立更新队列;React 合并同一优先级的更新,最终一次 render / commit 执行。
虚拟列表如何结合 Fiber 优化长列表?
只渲染可视区域节点,滚动触发增量 Fiber 更新,分帧渲染减少主线程压力。
高频更新怎么优化?
结合 RAF + React 自动批处理,合并多次状态更新,减少 render 次数,保证 UI 流畅。
落地总结
把这条主线记牢:setState → 更新队列 → Scheduler 调度 → WIP Fiber 树 → diff → commit。Render 树描述 UI,Fiber 管调度,DOM 才是真实页面。
无论有多少子组件同时 setState,React 都会通过批量更新 + Fiber 增量渲染,只触发必要的 render------这就是它高性能的底层答案。
关注我们,一起把技术讲明白
**寻码札记 **