news 2026/10/18 6:34:01

React 源码漫游:从 useState 到 Fiber,我终于知道自己写的代码去哪了

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
React 源码漫游:从 useState 到 Fiber,我终于知道自己写的代码去哪了

React 源码漫游:从useState到 Fiber,我终于知道自己写的代码去哪了

直到某天我盯着 setState 发呆:我调用了它,然后呢?它到底把我的状态放哪了?又是谁把页面变了的?

引子:一个让我开始怀疑人生的疑问

事情是这样的。

某天我写了一段再普通不过的代码:

jsx

function Counter() { const [count, setCount] = useState(0) console.log('render') return <button onClick={() => setCount(count + 1)}>{count}</button> }

每次点击,console.log('render')都会打印一次。我盯着这行日志,突然冒出一个问题:

"这个 count,每次都是新的变量,那 React 是怎么知道上一次的值是多少的?"

换句话说:useState 返回的 count,明明是个常量,为什么每次渲染都能拿到最新的值?

这个问题看起来简单,但它逼着我打开了 React 的源码仓库。而这一打开,就是一场从"会用"到"看懂"的漫长旅程。

第一章:从 JSX 到 ReactElement——代码的第一次变形

我写的 JSX:

jsx

<button onClick={handleClick}>点击</button>

编译后变成了:

js

React.createElement('button', { onClick: handleClick }, '点击')

再进一步,createElement返回的是一个普通对象,大概长这样:

js

{ type: 'button', props: { onClick: handleClick, children: '点击' }, key: null, ref: null, $$typeof: Symbol(react.element) }

这个对象,就叫 ReactElement。

它是"描述 UI 应该长什么样"的数据结构,不是 DOM,也不是组件实例。

看到这里我才明白:React 里写的 JSX,本质上就是在"用对象描述界面"。

而$$typeof: Symbol(react.element)这个字段,是 React 用来防止 XSS 的——因为 Symbol 无法被 JSON 序列化,所以服务端返回的恶意 JSON 无法伪装成 ReactElement。

一个小小的 Symbol,背后是一个安全设计。

第二章:Fiber——React 的"虚拟任务调度系统"

理解了 ReactElement,我遇到了 React 16 之后最核心的概念:Fiber。

我一开始以为 Fiber 是"虚拟 DOM 的升级版",后来发现完全不是。

Fiber 的本质,是一个"可以被中断、可以被打断、可以被恢复"的执行单元。

为什么需要 Fiber?

在 React 15 之前,React 的更新是递归的:

js

function reconcile(element) { // 处理当前节点 // 递归处理所有子节点 // 递归处理所有兄弟节点 }

递归的问题在于:一旦开始,就必须一口气执行完,不能中断。

如果组件树有几千个节点,一次更新可能要跑几百毫秒,期间浏览器完全没法响应输入——页面直接卡死。

React 16 引入 Fiber,把"递归"改成了"链表遍历":

js

{ type: 'div', child: ..., // 第一个子节点 sibling: ..., // 下一个兄弟节点 return: ..., // 父节点 ... }

这样,React 就可以"走一步、停一下",把控制权交还给浏览器,等有空了再继续。

这就是时间切片(Time Slicing)的基础。

Fiber 的两棵"树"

React 在内存里维护两棵 Fiber 树:

  • current 树:当前屏幕上显示的 UI。
  • workInProgress 树:正在构建的新 UI。

当 workInProgress 树构建完成,React 会把current指针指向它,完成一次"切换"。

这个过程,叫 Commit。

所以 React 的更新,本质上是"在内存里构建新树,然后一次性切换"。这也是为什么我们说 React 的渲染是"异步、可中断"的。

第三章:Render 与 Commit——更新的两个阶段

理解了 Fiber 后,React 的更新流程就清晰了:Render 阶段 + Commit 阶段。

Render 阶段:可中断的"计算"

Render 阶段做的事:

  1. 从根节点开始遍历 Fiber 树。
  2. 对比新旧 Fiber,标记出需要更新的节点(diff)。
  3. 执行组件的 render 函数,生成新的 ReactElement。

这个阶段是可中断的。如果浏览器需要响应高优先级任务(比如用户输入),React 会暂停 Render,先处理输入,再回来继续。

这也是为什么 render 函数必须是纯函数——因为它可能被多次调用、可能被中断、可能被丢弃。

Commit 阶段:不可中断的"落盘"

Commit 阶段做的事:

  1. 把 Render 阶段标记的变化,应用到真实 DOM。
  2. 执行生命周期函数(componentDidMount、useLayoutEffect 等)。

这个阶段是不可中断的。因为它直接操作 DOM,一旦开始就必须完成,否则用户会看到"半成品"。

这也是为什么 useEffect 是异步的、useLayoutEffect 是同步的——前者在 Commit 之后异步执行,不阻塞渲染;后者在 Commit 之后同步执行,会阻塞渲染。

第四章:Hooks 的秘密——链表

回到最初的问题:useState 是怎么记住状态的?

答案藏在 Hooks 的"链表"结构里。

每个函数组件对应一个 Fiber 节点,Fiber 节点上有一个memoizedState属性,它指向一个 Hooks 链表:

js

fiber.memoizedState = { memoizedState: 0, // useState 的值 queue: {...}, // 更新队列 next: { memoizedState: null, // useEffect 的依赖 next: { memoizedState: {...}, // useRef 的值 next: null } } }

每次调用 useState,React 就从链表上取一个 Hook,按顺序对应。

这也解释了Hooks 的两条铁律:

  1. 只能在函数组件顶层调用——因为 Hooks 是按顺序存储的,放到条件语句里会导致顺序错乱。
  2. 不能放到循环里——循环次数变化,Hooks 数量就变了,链表就对不上了。

我之前一直觉得这些规则"很烦",理解了链表结构后才发现:它们不是规定,是物理约束。

第五章:setState 之后发生了什么?

现在我可以回答最初那个问题了:setCount 之后发生了什么?

jsx

setCount(count + 1)
  1. 创建 Update 对象:{ action: count + 1, next: … }。
  2. 加入更新队列:这个队列挂在 Hook 的 queue 上。
  3. 调度更新:调用 scheduleUpdateOnFiber,从当前 Fiber 向上找到根节点,标记需要更新。
  4. Render 阶段:遍历 Fiber 树,执行组件函数,处理更新队列,算出新的 state。
  5. Commit 阶段:把变化应用到 DOM。

最妙的是"更新队列"的设计:

jsx

setCount(c => c + 1) setCount(c => c + 1) setCount(c => c + 1)

这三个调用不会立刻执行,而是被放进队列,等 Render 阶段一起处理。

这就是为什么函数式更新能拿到"最新值"——因为它们是按顺序在队列里执行的,而不是基于渲染时的快照。

第六章:diff 算法——React 的"最小改动"哲学

React 的 diff 算法,核心是三个假设:

  1. 只比较同层节点,不跨层比较。
  2. type 不同,直接替换。
  3. 用 key 标识同层节点,避免误判。
  4. jsx
// 旧 <ul> <li key="a">A</li> <li key="b">B</li> </ul> // 新 <ul> <li key="b">B</li> <li key="a">A</li> </ul>

有了 key,React 知道这两个节点是"换了位置",而不是"内容变了"。

没有 key,React 会按顺序对比,导致大量不必要的销毁和重建。

这就是为什么"用 index 当 key"是危险的——列表顺序变化时,index 变了,React 会误以为节点变了。

第七章:并发特性——React 的"未来"

理解了 Fiber 之后,React 18 的并发特性就顺理成章了。

useTransition:把更新标记为"不紧急"

jsx

const [isPending, startTransition] = useTransition() startTransition(() => { setSearchQuery(input) // 这个更新可以被中断 })

React 会先处理紧急更新(比如输入框),再处理过渡更新。

useDeferredValue:延迟某个值

jsx

const deferredQuery = useDeferredValue(query)

它返回一个"延迟版"的值,让昂贵的渲染可以晚一点执行。

Suspense:等待异步数据

jsx

<Suspense fallback={<Loading />}> <Profile /> </Suspense>

当 Profile 还没准备好时,先显示 Loading。

这些特性的共同基础,都是 Fiber 的"可中断、可调度"能力。

终章:读懂源码之后,我变了

理解 React 源码后,我并没有变成什么"源码大神",但我确实变了:

  1. 我不再害怕"为什么 render 了两次"——因为我知道 StrictMode 会故意双调用。
  2. 我不再乱用 useMemo——因为我知道它本身也有开销。
  3. 我理解了 Hooks 的规则从何而来——因为它们是链表结构的必然结果。
  4. 我不再觉得 React 是"魔法"——它只是一个精密的调度器 + 一个高效的 diff 算法 + 一个巧妙的链表结构。

最重要的是:我不再只是"会写 React",而是开始"理解 React 在做什么"。

P.S. 写完这篇文章后,我又在 useEffect 的依赖数组里漏了一个变量。看来,读懂源码和写好代码之间,还隔着 1000 个小时的实践。

P.P.S. React 源码很复杂,但它的核心思想其实很简单:UI 是状态的函数。其余的一切,都是为了让这句话在浏览器里高效地成立。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/18 6:33:57

TCP TIME_WAIT原理与高并发优化实战指南

1. 项目概述&#xff1a;TIME_WAIT不是故障&#xff0c;而是TCP协议的“安全守门人”你刚在服务器上执行netstat -an | grep TIME_WAIT | wc -l&#xff0c;终端回显数字跳到8000&#xff0c;心跳一紧——是不是被攻击了&#xff1f;服务要崩了&#xff1f;别急&#xff0c;先放…

作者头像 李华
网站建设 2026/10/18 6:32:41

跨境电商前端HTML模板:多语言多币种可落地骨架

简介&#xff1a;这是一套面向前端初学者与电商项目开发者的跨境电商网站前端模板代码&#xff0c;基于HTMLCSSJS构建&#xff0c;可快速搭建具备商品展示、购物车、响应式布局等基础功能的电商页面。资源共188个文件&#xff0c;包含26个结构清晰的HTML页面、7个核心CSS样式表…

作者头像 李华
网站建设 2026/10/18 6:32:00

Proteus仿真12864液晶屏:51单片机ST7920驱动与串行时序解析

简介&#xff1a;51单片机12864大液晶屏Proteus仿真设计资源&#xff0c;适合电子类专业课程设计、毕业设计&#xff0c;以及正在学习单片机显示驱动的初学者。工程以Proteus仿真替代实物焊接&#xff0c;演示了如何用C语言控制12864点阵液晶屏完成初始化、字符与图形显示&…

作者头像 李华
网站建设 2026/10/18 6:31:32

SpringBoot+Vue健身房管理系统设计与实现全解析

1. 项目概述与业务价值拆解1.1 这个管理系统到底解决什么问题先说结论&#xff1a;健身房管理系统是目前JavaWeb毕设里性价比极高的一类选题。它不像电商系统那样业务链路冗长&#xff0c;也不像纯后台管理系统那样缺乏亮点&#xff0c;而是处在一个恰到好处的位置——业务逻辑…

作者头像 李华
网站建设 2026/10/18 6:31:21

C++类型转换全解析:隐式规则、四种强制转换与工程避坑指南

1. 开篇&#xff1a;类型转换为什么是C开发里的“暗雷区”熟悉C的人应该都有过这种体验&#xff1a;代码写起来很爽&#xff0c;但一遇到类型转换&#xff0c;尤其在跨平台、多线程或者接手老项目时&#xff0c;各种隐式转换和强转混在一起&#xff0c;编译能过、运行崩溃、数据…

作者头像 李华
网站建设 2026/10/18 6:31:02

多人协同开发工具链实战:版本控制、任务管理与接口联调

1. 协同开发的核心痛点与工具选型逻辑多人协作写代码这件事&#xff0c;没经历过的人觉得就是“几个人一起写”&#xff0c;经历过的人才知道&#xff0c;真正让人头疼的从来不是代码本身&#xff0c;而是代码之外的那些事&#xff1a;谁改了哪个文件、什么时候改的、为什么改、…

作者头像 李华