一文吃透React核心:从问题到流程全解析

94 阅读11分钟

很多人学React时,先被JSX、Hooks、Fiber这些概念绕晕,越学越觉得“抽象”。其实核心不是记API,而是搞懂React到底在解决什么问题、按什么逻辑运行。这篇文章从底层逻辑出发,把React的核心原理拆成大白话,不管是面试还是实际开发,都能帮你打通任督二脉。

一、React 到底在解决什么问题?

先抛掉“React 是个库”这种废话

很多入门教程一上来就说“React是一个用于构建用户界面的JavaScript库”,这句话没错但毫无意义。React真正的价值,是解决了前端开发中“UI更新混乱、性能拉胯”的核心痛点,具体落地成三件事:

  1. 用 JS 描述 UI(声明式):不用手动操作DOM,只告诉React“我要什么样的UI”,而非“怎么做出这个UI”;

  2. 高效计算 UI 的变化(Diff + Fiber):页面更新时,不盲目重绘整个DOM,只找变化的部分;

  3. 可控地、分优先级地更新视图(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是“执行单位”,两者本质不同,用表格对比更清晰:

ReactElementFiber
本质是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: Hook1Hook2Hook3, // 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底层有清晰理解,轻松碾压面试官:

  1. React 首先把 JSX 转成 ReactElement,完成UI描述;

  2. 再把 ReactElement 组织成 Fiber 树,将UI描述转化为可执行的工作单元;

  3. 更新时在 workInProgress 树上进行可中断的 Render 阶段,计算差异并标记副作用;

  4. Render 阶段只计算差异,不操作 DOM;

  5. Commit 阶段一次性执行所有副作用,更新真实 DOM,且不可中断;

  6. 通过 Scheduler 和 Lane 机制保证高优先级任务优先响应,避免页面卡顿。

写在最后

学React不要陷入“记API、背用法”的误区,先搞懂它的核心逻辑:从UI描述到工作单元,从差异计算到调度更新,每一步都是为了解决“高效、可控地更新UI”这个核心问题。理解了这些底层原理,不管是使用Hooks、排查性能问题,还是应对面试,都能游刃有余。