很多人学React时,先被JSX、Hooks、Fiber这些概念绕晕,越学越觉得“抽象”。其实核心不是记API,而是搞懂React到底在解决什么问题、按什么逻辑运行。这篇文章从底层逻辑出发,把React的核心原理拆成大白话,不管是面试还是实际开发,都能帮你打通任督二脉。
一、React 到底在解决什么问题?
先抛掉“React 是个库”这种废话
很多入门教程一上来就说“React是一个用于构建用户界面的JavaScript库”,这句话没错但毫无意义。React真正的价值,是解决了前端开发中“UI更新混乱、性能拉胯”的核心痛点,具体落地成三件事:
-
用 JS 描述 UI(声明式):不用手动操作DOM,只告诉React“我要什么样的UI”,而非“怎么做出这个UI”;
-
高效计算 UI 的变化(Diff + Fiber):页面更新时,不盲目重绘整个DOM,只找变化的部分;
-
可控地、分优先级地更新视图(Scheduler):优先响应用户交互(比如打字、点击),再处理耗时的渲染任务,避免页面卡顿。
一句话总结(记死,面试直接用):
React = UI 描述 + 更新计算 + 调度执行
这三个部分环环相扣,构成了React的核心骨架。
二、React 的整体运行流程(全局鸟瞰)
先给你一张“口述流程图”(面试版)
React的运行流程本质是“从描述到执行”的过程,用极简流程图概括如下,面试时能流畅说出来,基本就能碾压一半候选人:
JSX
↓
ReactElement(UI 描述)
↓
Fiber Tree(工作单元)
↓
Render 阶段(计算差异)
↓
Commit 阶段(更新 DOM)
再记一句核心口诀,帮你分清各阶段角色:
JSX 只是描述,Fiber 才是执行单位,DOM 更新只发生在 commit 阶段
这句话能帮你避开很多认知误区,比如“JSX直接生成DOM”“Fiber就是虚拟DOM”这类错误理解。
三、从 JSX 说起(为什么不是直接操作 DOM)
很多人第一次写JSX时会疑惑:为什么不直接用document.createElement创建DOM?反而要多一层JSX转换?核心原因是“直接操作DOM成本太高,且难以维护”,而JSX本质是“UI描述的语法糖”。
示例:一段简单的JSX
function App() {
return <h1>Hello</h1>
}
JSX 编译后 ≈ 原生JS调用
浏览器无法直接识别JSX,需要通过Babel等工具编译,编译后会转换成React.createElement方法的调用:
React.createElement(
'h1', // 标签类型
null, // 标签属性(这里无属性,传null)
'Hello' // 子元素
)
编译后得到的是什么?
答案是 ReactElement(不是DOM)。它是一个普通的JavaScript对象,用来描述UI的结构,本质是一次“UI描述快照”,代码示意如下:
{
$$typeof: Symbol(react.element), // 标记这是React元素
type: 'h1', // 元素类型(标签名/组件)
props: { children: 'Hello' }, // 元素属性(含子元素)
// 还有key、ref等可选属性
}
关键理解点(面试常问)
ReactElement 是一次 UI 描述快照,类似:虚拟 DOM、配置对象、UI 蓝图
这里要重点区分:ReactElement是“结果描述”,不是“创建DOM的过程”。它就像一张建筑图纸,告诉你最终要建成什么样,但本身不是房子(DOM)。这样设计的好处是:用极低成本的JS对象替代高成本的DOM操作,后续计算差异时,只操作这些对象即可。
四、为什么需要 Fiber?(核心思想)
Fiber是React 16引入的核心机制,也是面试的高频难点。要理解Fiber,先搞懂它要解决的问题——React 15的性能瓶颈。
问题背景(面试官很爱问)
React 15 的问题
-
采用递归diff算法遍历DOM树,计算UI差异;
-
更新过程一旦开始,就无法中断,必须一次性执行完;
-
如果要更新一个包含上千个节点的列表,递归遍历会占用主线程很久,导致用户交互(打字、点击)、动画等操作卡顿。
举个例子 🌰
setState(() => {
// 更新一个 5000 项列表
})
假设用户此时正在输入框打字,React 15和React 16(Fiber)的表现完全不同:
-
React 15:不管用户输入,先把5000项列表的差异算完,主线程被占满,用户输入无响应,页面卡顿;
-
React Fiber:优先响应用户输入,暂停列表更新计算,等用户输入结束后,再恢复计算,页面流畅无卡顿。
Fiber 是什么?(一句话版)
Fiber 是一个 可中断、可恢复、带优先级的工作单元
这里要纠正一个常见误区:Fiber ≠ 虚拟DOM。虚拟DOM是“UI描述”,而Fiber是“执行单位”,两者本质不同,用表格对比更清晰:
| ReactElement | Fiber |
|---|---|
| 本质是UI描述(快照) | 本质是工作单元(执行任务) |
| 创建后不会变化 | 更新时可复用、可修改状态 |
| 无状态,只存结构信息 | 有状态,记录工作进度、优先级等 |
五、Fiber Tree 是什么结构?(为什么是链表)
Fiber Tree是由Fiber节点构成的树状结构,也是React的工作树。和React 15的递归树不同,Fiber Tree采用链表结构,每个Fiber节点只关心三件事,对应三个指针:
父节点 → return 指针
第一个子节点 → child 指针
下一个兄弟节点 → sibling 指针
结构示意
比如我们有这样的UI结构:
App
└─ div
├─ h1
└─ span
用Fiber链表表示的实际结构的是:
App
↓ child(指向第一个子节点div)
div
↓ child(指向第一个子节点h1) → sibling(指向兄弟节点span)
h1 ---------------------------------> span
为什么不用递归?
核心原因是链表结构支持“可中断、可恢复”的遍历,而递归不行。具体来说:
-
链表遍历可以随时暂停,记录当前遍历到的节点(通过三个指针);
-
等主线程空闲后,再通过记录的指针恢复遍历,继续执行工作;
-
递归一旦开始,就必须走到最后,无法中途暂停,只能一直占用主线程。
面试时可以用这句话总结:
Fiber 把递归遍历拆成可暂停的循环任务,从而解决了主线程阻塞的问题。
六、双缓存 Fiber 树(React 的“影分身术”)
为了避免“一边计算差异,一边更新DOM”导致的页面抖动,React采用了“双缓存”机制,同时维护两棵Fiber树,各司其职。
两棵树的作用
| 树名 | 作用 |
|---|---|
| current | 当前屏幕上显示的Fiber树,对应真实DOM结构 |
| workInProgress | 后台正在计算、构建的Fiber树,基于current树复制而来,用于计算更新差异 |
更新流程 🌰
current(屏幕上的)
↓ 复制一份作为基础
workInProgress(后台计算差异、构建新树)
↓ 计算完成后
commit(一次性更新DOM)
↓ 指针切换
current 指向新树,workInProgress 清空等待下次更新
为什么要两棵树?
核心目的是“无副作用计算”。如果只有一棵树,计算差异时直接修改树结构,可能导致DOM更新不完整、页面出现中间状态(比如一半旧UI、一半新UI)。双缓存机制让计算和更新分离:后台在workInProgress树上安心计算,计算完再一次性替换current树并更新DOM,避免页面抖动。
面试时可以一句话概括:
每个Fiber节点通过alternate指针指向另一棵树的对应节点,实现无副作用的差异计算。
七、Render 阶段 vs Commit 阶段(必讲清)
React的更新过程分为两个核心阶段:Render阶段和Commit阶段,两者的职责、特点完全不同,也是面试高频考点。
Render 阶段(可以被打断)
做什么?
-
基于ReactElement创建/更新Fiber节点,构建workInProgress树;
-
对比current树和workInProgress树的差异,标记出需要执行的操作(比如插入、删除、修改DOM),这些操作被称为“副作用”(用flags标记);
-
确定每个Fiber节点的更新优先级。
特点
-
❌ 不操作真实DOM,只做计算和标记;
-
✅ 可暂停、可中断、可丢弃(如果有更高优先级任务进来,直接放弃当前计算,重新开始);
-
✅ 完全在内存中执行,不影响页面展示。
Commit 阶段(一次性执行)
做什么?
-
执行Render阶段标记的副作用,比如插入、删除、修改真实DOM;
-
执行组件的生命周期方法(比如componentDidMount、componentDidUpdate)和useEffect钩子;
-
切换current树和workInProgress树的指针,完成更新。
特点
-
❌ 不可中断,必须一次性执行完(否则会导致DOM状态不一致,页面出现异常);
-
✅ 直接操作真实DOM,是唯一会影响页面展示的阶段;
-
✅ 执行速度快,因为Render阶段已经做好了所有计算,这里只做“执行”工作。
面试标准总结(背下来)
Render 阶段是“算”(计算差异、标记副作用),Commit 阶段是“做”(执行副作用、更新DOM)。
八、更新从哪来?(setState 的整体模型)
我们常用的setState、useState更新状态,本质是触发React的更新流程。以setState为例,背后的逻辑流程很简单,却能帮你理解React的更新触发机制。
setCount(c => c + 1)
这句代码背后的完整逻辑流程:
setState(触发更新)
↓
创建 update 对象(记录更新内容、优先级等信息)
↓
将 update 对象放入对应组件的 updateQueue(更新队列)
↓
通过 Lane 机制标记该更新的优先级
↓
Scheduler(调度器)根据优先级,安排进入Render阶段
关键点理解:
setState 本身不更新视图,它只是“登记一次变更请求”。
也就是说,调用setState后,不会立刻执行更新,而是先把更新请求加入队列,再由调度器根据优先级安排执行。这也是为什么setState是“异步”的——它需要等待调度器的安排,而非立即更新DOM。
九、优先级 & Scheduler(为什么不卡)
Fiber解决了“可中断”问题,而Scheduler(调度器)解决了“什么时候执行”的问题,两者结合让React能够优先响应高优先级任务,避免页面卡顿。
不同更新,不同优先级
React根据任务的紧急程度,给更新划分了不同优先级,常见优先级排序(从高到低):
| 更新类型 | 优先级 | 说明 |
|---|---|---|
| 用户输入(打字、点击) | 高 | 必须立即响应,否则影响交互体验 |
| 动画效果(CSS动画、过渡) | 中 | 需要流畅执行,避免卡顿 |
| 列表渲染、数据加载 | 低 | 可延迟执行,不影响核心交互 |
🌰 举例
-
用户在输入框打字时,输入对应的更新任务优先级最高,Scheduler会暂停当前正在执行的低优先级任务(比如列表渲染),先响应输入;
-
用户输入结束后,Scheduler再重新调度低优先级任务,分帧完成列表渲染(每帧执行一小部分,不占用主线程过久)。
面试一句话总结:
Scheduler 决定“什么时候算”(调度任务执行时机),Fiber 决定“算什么”(具体的更新工作单元)。
十、Hooks 放在整体里的位置
Hooks(比如useState、useEffect)是React 16.8引入的特性,本质是“组件状态在Fiber上的表达方式”,它的底层依然依赖Fiber树的状态管理。
Hooks 存在哪里?
每个组件对应的Fiber节点上,有一个memoizedState属性,Hooks就存储在这个属性中,以链表的形式排列。比如:
// Fiber节点结构(简化)
{
type: App,
memoizedState: Hook1 → Hook2 → Hook3, // Hooks链表
// 其他属性
}
当组件调用useState、useEffect时,React会沿着memoizedState的链表依次查找、创建或更新对应的Hook。
为什么不能写在if里?
这是面试高频问题,核心原因是Hooks依赖调用顺序。如果把Hooks写在if、for等条件语句中,会打乱链表的顺序,导致React无法正确匹配之前的Hook状态,出现bug。
// 错误示例
function App() {
if (condition) {
const [count, setCount] = useState(0); // 可能被跳过,打乱链表顺序
}
const [name, setName] = useState('');
return <div>...</div>
}
十一、整体再压缩成 6 句话(面试王炸)
最后把整个React核心逻辑压缩成6句话,面试时能流畅说出来,基本能证明你对React底层有清晰理解,轻松碾压面试官:
-
React 首先把 JSX 转成 ReactElement,完成UI描述;
-
再把 ReactElement 组织成 Fiber 树,将UI描述转化为可执行的工作单元;
-
更新时在 workInProgress 树上进行可中断的 Render 阶段,计算差异并标记副作用;
-
Render 阶段只计算差异,不操作 DOM;
-
Commit 阶段一次性执行所有副作用,更新真实 DOM,且不可中断;
-
通过 Scheduler 和 Lane 机制保证高优先级任务优先响应,避免页面卡顿。
写在最后
学React不要陷入“记API、背用法”的误区,先搞懂它的核心逻辑:从UI描述到工作单元,从差异计算到调度更新,每一步都是为了解决“高效、可控地更新UI”这个核心问题。理解了这些底层原理,不管是使用Hooks、排查性能问题,还是应对面试,都能游刃有余。