React 原理与源码学习笔记
适合目标:系统理解 React 的运行机制,能把 Fiber、Diff、Hooks、调度、批处理这些高频源码面试题讲清楚。
学习定位:这一份偏“能解释原理、能看懂主干源码、能回答底层问题”。
学习原则:先搭地图,再看细节;先讲流程,再钻源码;先掌握主干概念,再补实现细节。
目录
- 学习总览
- 看源码前先建立地图
- React 整体运行流程
- Fiber 架构
- 双缓冲机制
- 调度、优先级与时间切片
- Diff 与 Reconciliation
- Render 与 Commit
- Hooks 原理
- 更新队列与批处理
- React 18 并发特性
- 事件系统与上下文更新
- 常见源码面试题
- 8 小时源码学习规划
- 一页速记总结
- 背诵口诀
- 源码阅读路线
1. 学习总览
1.1 为什么要学 React 原理
只会写 React 和理解 React 底层,是两个层次。
学源码和原理的意义:
- 面试能讲清楚“为什么”
- 性能优化更有依据
- 理解 Hook、渲染、更新时机不再靠死记
- 遇到复杂问题时更容易定位本质
1.2 React 原理到底学什么
你真正需要掌握的主线只有这几条:
- 组件更新是怎么被调度的
- React 为什么要引入 Fiber
- 新旧节点是怎么比较的
- Hook 状态为什么靠顺序关联
- 更新为什么能批处理、可中断、可恢复
1.3 React 源码学习主线
可以记成:
JSX -> React Element -> Fiber -> 调度 -> Render -> Commit -> DOM 更新
也就是说:
- JSX 最终变成元素对象
- 元素对象对应 Fiber 节点
- Fiber 节点组成 Fiber 树
- 更新被调度
- Render 阶段生成新的工作树
- Commit 阶段把变更提交到宿主环境
1.4 面试里最常考的底层话题
- 为什么需要 Fiber
- React Diff 为什么是 O(n)
- key 的作用是什么
- render 和 commit 有什么区别
- useState 为什么不能条件调用
- useEffect 为什么有清理函数
- React 18 自动批处理和并发更新是什么
2. 看源码前先建立地图
2.1 React 不是一个包,而是一组包
React 仓库不是只有一个核心文件,而是由多个包组成。
从理解层面可以先记住几个重点:
reactreact-domreact-reconcilerschedulershared
2.2 这些包分别负责什么
react
负责暴露开发者常用 API,例如:
useStateuseEffectcreateContextmemo
react-dom
负责和浏览器 DOM 环境打交道,例如:
- 创建 root
- 挂载到页面
- 处理宿主环境更新
react-reconciler
这是 React 内部协调器的核心,很多 Fiber、Diff、Hook、调度主逻辑都在这里。
scheduler
负责调度任务,安排不同优先级的工作。
shared
放一些多个包共享的工具和常量。
2.3 学源码时最容易犯的错
- 一上来就逐行啃源码
- 没有流程图,只盯实现细节
- 把不同版本实现细节混在一起
- 把“教学化理解”和“真实实现”混为一谈
2.4 正确阅读策略
推荐顺序:
- 先理解概念和整体流程
- 再理解 Fiber 结构
- 再理解 render/commit
- 再理解 Hook 和 update queue
- 最后看调度与优先级
3. React 整体运行流程
3.1 从 JSX 到页面更新的大流程
一个典型更新过程可以理解成:
- JSX 被编译成 React Element
- React Element 被转换为 Fiber 节点
- 更新进入调度系统
- Render 阶段构建 workInProgress Fiber 树
- 找出需要变更的副作用
- Commit 阶段把副作用提交到 DOM
3.2 React Element 是什么
React Element 不是 DOM,也不是 Fiber。
它更像一个普通对象,用来描述:
- 元素类型
- key
- props
- ref
你可以把它理解成:
对界面的轻量描述对象
3.3 Fiber 又是什么
Fiber 是 React 内部的工作单元节点。
它比 React Element 更强,因为它不仅描述 UI,还承载:
- 更新信息
- 优先级
- 副作用标记
- 父子兄弟关系
- Hook 状态
3.4 一句话串起来
Element 更像“描述”,Fiber 更像“可调度的工作节点”。
4. Fiber 架构
Fiber 是 React 原理里最核心的知识点。
4.1 为什么需要 Fiber
早期 React 的递归协调过程有一个核心问题:
一旦开始,不能中断
如果组件树很大,更新工作会长时间占用主线程,造成:
- 页面卡顿
- 用户输入延迟
- 动画掉帧
所以 React 需要一种新结构,让更新工作:
- 可拆分
- 可中断
- 可恢复
- 可按优先级处理
这就是 Fiber 的意义。
4.2 Fiber 的本质
Fiber 可以理解成:
一个虚拟栈帧 + 一个工作单元 + 一个链表节点
为什么这样理解:
- 它能表示组件节点
- 它能保存当前工作进度
- 它能让 React 用链表结构而不是纯递归调用栈来遍历树
4.3 Fiber 节点上常见信息
面试里不用死背全部字段,但要知道它大概保存什么:
tag:节点类型key:列表稳定标识type:组件/标签类型stateNode:真实 DOM 或类组件实例等return:父 Fiberchild:第一个子 Fibersibling:下一个兄弟 Fiberalternate:当前树和工作树之间的对应节点pendingProps:待处理 propsmemoizedProps:上次已生效 propsmemoizedState:上次已生效 state / Hook 链表updateQueue:更新队列flags:当前节点副作用标记subtreeFlags:子树副作用标记lanes:优先级相关信息
4.4 Fiber 的树结构为什么不是普通数组
React 常用的是:
childsiblingreturn
这种链式结构的好处:
- 节省内存
- 遍历灵活
- 更适合增量处理
4.5 工作单元怎么理解
React 在 render 阶段会把每个 Fiber 当成一个“工作单元”。
每做完一小块,就可以决定:
- 继续做
- 暂停
- 让更高优先级任务先做
4.6 Fiber 记忆口诀
Fiber 不是普通虚拟 DOM 节点,它是可调度、可中断、可恢复的工作单元。
5. 双缓冲机制
5.1 什么是双缓冲
React 内部会维护两棵树:
- 当前已经展示在页面上的树:
current - 正在构建的新树:
workInProgress
这就是双缓冲思想。
5.2 为什么要有两棵树
因为 React 不希望边算边改真实 DOM。
更合理的流程是:
- 先基于旧树构建一棵新工作树
- 在新树上完成比较和副作用收集
- 确认无误后一次性提交
这样好处很多:
- 计算过程更安全
- 可以中断
- 可以恢复
- 页面上的旧树始终稳定
5.3 alternate 指针是什么
alternate 用来连接 current Fiber 和它对应的 workInProgress Fiber。
可以简单理解成:
current <-> workInProgress
5.4 双缓冲切换过程
- 页面当前使用 current 树
- 更新发生,React 以 current 为基础创建/复用 workInProgress 树
- render 阶段在 workInProgress 上工作
- commit 完成后,workInProgress 变成新的 current
5.5 记忆口诀
页面看 current,更新算 WIP,提交后两者交换身份。
6. 调度、优先级与时间切片
6.1 调度解决什么问题
React 不只是“更新”,还要决定:
- 哪个更新先做
- 哪个更新后做
- 是否需要打断当前工作
- 当前这一帧还能不能继续做事
这就是调度的任务。
6.2 优先级为什么重要
不是所有更新都一样紧急。
例如:
- 输入框输入很紧急
- 搜索结果列表刷新可以稍慢一点
- 后台低优先级更新更不紧急
React 需要区分优先级,才能让界面更流畅。
6.3 Lanes 怎么理解
现代 React 中,优先级更多通过 Lanes 这套机制表达。
你可以把 lane 理解成:
带优先级信息的更新通道
它的意义:
- 区分不同更新优先级
- 支持批量合并
- 支持并发调度
6.4 时间切片是什么
时间切片可以简单理解成:
把大任务拆成很多小段,在浏览器空档中分段处理
这样 React 就不需要一次把整棵树都同步算完。
6.5 requestIdleCallback 和 React 的关系
这是高频易错点。
很多教程会用 requestIdleCallback 来帮助理解“时间切片”,但你在源码和面试里最好说得更严谨:
- React 的调度思想可以类比空闲时间处理任务
- 但实际实现并不等于简单直接依赖
requestIdleCallback - React 有自己的
scheduler机制,会综合使用更稳定的调度方式
所以更稳的答法是:
React 的时间切片思想可以类比 requestIdleCallback,但真实实现是更复杂的协作式调度。
6.6 同步更新和并发更新
同步更新
优先级高,通常要尽快完成。
并发更新
允许更新过程被打断,让更紧急的任务先执行。
6.7 调度流程的理解版
可以简单记为:
- 更新发生
- React 给更新分配优先级
- 调度器决定什么时候执行
- render 阶段按优先级处理 Fiber 工作
- 必要时中断和恢复
6.8 记忆口诀
调度管先后,Lane 管优先级,时间切片管分段执行。
7. Diff 与 Reconciliation
React 的 Diff 题,是面试超级高频。
7.1 Reconciliation 是什么
Reconciliation 中文常译为“协调”。
它解决的问题是:
状态变了以后,React 如何比较新旧树,并决定最小更新代价
7.2 为什么 React Diff 是 O(n)
React 没有去做通用树的最优 Diff,那样复杂度太高。
React 使用了两个重要假设:
- 不同类型的元素会生成不同子树
- 同层节点通过 key 进行稳定识别
基于这两个前提,React 才能把复杂度控制在较低水平。
7.3 同层比较原则
React 默认只在同层比较节点,不会跨层做复杂移动推断。
这也是面试常问点:
为什么 React 只做同层比较?
因为这样可以显著降低算法复杂度,换来更可接受的性能和实现成本。
7.4 key 的作用
key 用来帮助 React 在同层节点中识别“谁是谁”。
没有稳定 key 时,React 容易:
- 误删节点
- 错误复用节点
- 导致输入状态错乱
7.5 key 用 index 有什么问题
如果列表只是静态展示,index 问题不大。
但如果列表有:
- 插入
- 删除
- 排序
- 过滤
那么 index 会让节点身份不稳定,导致:
- 状态错位
- 额外移动
- DOM 复用错误
7.6 Diff 时常见三种变化
- 新增
- 删除
- 移动 / 更新
React 会通过副作用标记把这些操作记录下来。
7.7 Diff 的理解版流程
- 从根开始比较新旧节点
- 类型不同,直接替换整棵子树
- 类型相同,尽量复用节点
- 再进入子节点比较
- 对列表节点借助 key 做识别
- 生成 Placement / Update / Deletion 等副作用信息
7.8 Diff 记忆口诀
不同类型直接换,同层比较求高效,列表比较靠 key。
8. Render 与 Commit
8.1 React 更新为什么分两个阶段
React 把更新流程拆成:
- render 阶段
- commit 阶段
这样可以把“计算”和“真正变更”分开。
8.2 Render 阶段做什么
render 阶段主要做:
- 处理更新队列
- 执行组件函数 / 生命周期相关逻辑
- 生成新的 Fiber 树
- 比较新旧节点
- 收集副作用
特点:
- 可以中断
- 可以恢复
- 不直接改真实 DOM
8.3 Commit 阶段做什么
commit 阶段主要做:
- 把副作用真正提交到 DOM
- 执行 ref 相关操作
- 执行 layout effect
- 安排 passive effect
特点:
- 不可中断
- 需要尽快完成
- 会直接影响用户看到的页面
8.4 beginWork 和 completeWork
这是 render 阶段里很重要的两个概念。
beginWork
主要负责:
- 处理当前 Fiber
- 计算子节点
- 决定是否继续向下遍历
completeWork
主要负责:
- 子树处理完成后的收尾
- 冒泡副作用标记
- 为宿主节点准备更新信息
8.5 Commit 的几个子阶段
理解层面可以分成:
- before mutation
- mutation
- layout
- passive effect(异步/后置)
before mutation
做提交前准备。
mutation
真正执行 DOM 插入、删除、更新。
layout
执行 useLayoutEffect、ref 相关同步逻辑。
passive effect
执行 useEffect 这类后置副作用。
8.6 为什么 render 可中断但 commit 不可中断
因为:
- render 阶段只是计算
- commit 阶段已经开始改真实界面
如果 commit 被中断,页面可能处于不一致状态。
8.7 记忆口诀
render 负责算,commit 负责改;render 可打断,commit 要一口气做完。
9. Hooks 原理
Hooks 原理和使用层最关键的桥梁,就是“顺序 + 链表 + 当前 Fiber”。
9.1 Hook 状态存在哪里
Hook 状态并不是存在函数里,而是挂在当前 Fiber 上。
可以粗略理解为:
- 每个函数组件对应一个 Fiber
- 这个 Fiber 上会挂一条 Hook 链表
- 每次渲染按顺序取出或创建对应 Hook 节点
9.2 为什么 Hook 不能条件调用
因为 React 不是根据 Hook 名字找状态,而是根据:
本次渲染中的调用顺序
例如第一次渲染:
- 第一个 Hook 是
useState - 第二个 Hook 是
useEffect
如果下一次渲染你把第一个 Hook 跳过了,后面的 Hook 都会错位。
这就是规则:
Hook 必须稳定按相同顺序调用
9.3 useState 原理怎么讲
useState 本质上可以理解为:
- Fiber 上有对应 Hook 节点
- Hook 节点里保存当前状态和更新队列
- 调用 setter 时,不是立即改值,而是往队列里放 update
- 下一轮渲染时,React 取出队列依次计算新状态
9.4 useState 的更新队列
当你调用:
setCount((c) => c + 1);
React 会把这次更新包装成 update 对象,放入队列。
下一轮 render 时:
- 取出旧 state
- 按顺序处理每个 update
- 计算出最终 state
9.5 为什么函数式更新很重要
因为队列里每个 update 都会基于前一个结果继续算,所以函数式更新更适合多次连续更新。
9.6 useEffect 原理怎么讲
可以理解为:
- render 阶段记录 effect 信息
- 比较依赖数组,判断是否需要执行
- commit 后统一执行 passive effect
- 在下一次 effect 执行前或卸载前执行 cleanup
9.7 useEffect 为什么有清理函数
因为很多副作用都需要成对管理:
- 订阅和取消订阅
- 定时器创建和清理
- 事件绑定和解绑
- 请求开始和中止
React 让 cleanup 成为 effect 逻辑的一部分,方便资源回收。
9.8 useMemo 和 useCallback 原理怎么理解
本质都属于:
在 Hook 链表中保存上一次的依赖和结果
如果依赖没变:
useMemo复用旧值useCallback复用旧函数引用
9.9 useReducer 原理怎么讲
你可以把它理解成 useState 的增强版:
- 也有 Hook 节点
- 也有更新队列
- 只是更新时会经过 reducer 计算
9.10 Hooks 原理记忆口诀
Hook 挂在 Fiber 上,靠调用顺序对应;state 走更新队列,effect 在提交后执行。
10. 更新队列与批处理
10.1 更新队列是什么
React 不会一调用 setState 就立刻同步改最终状态,而是把更新放入队列。
这样做的好处:
- 能合并多个更新
- 能按优先级处理
- 能在 render 阶段统一计算
10.2 类组件和函数组件都有更新队列
虽然实现细节不同,但思想类似:
- 收集更新
- 合并更新
- 在合适时机重新渲染
10.3 批处理是什么
批处理就是:
把同一批次里的多个更新合并处理,减少重复渲染
10.4 React 18 自动批处理
React 18 中,自动批处理覆盖了更多场景。
面试答法:
- 多个状态更新会尽量合并成一次渲染
- 这样减少性能损耗
- 是 React 18 的重要特性之一
10.5 为什么说 state 更新“不是同步立刻生效”
因为:
- 调用 setter 只是发起更新
- React 要把更新放入队列并调度
- 真正的新状态要等下一轮渲染计算后才能拿到
10.6 flushSync 怎么理解
在特殊情况下,可以使用同步刷新能力强制更早提交更新。
但面试和实际项目里要强调:
它是特殊手段,不应滥用。
10.7 记忆口诀
先入队,再调度;先合并,再渲染。
11. React 18 并发特性
11.1 并发渲染不是多线程
这是非常高频的纠错点。
React 18 的并发渲染不是说 React 开了多个线程同时执行 JS,而是:
- 更新工作可被打断
- 不同更新有不同优先级
- React 能让更紧急的更新先完成
11.2 startTransition / useTransition
这类能力的核心价值是:
把不那么紧急的状态更新标记为低优先级更新。
例子:
- 输入框文字更新很紧急
- 根据输入筛选大列表可以稍晚一点
11.3 Suspense 原理化理解
可以先从理解层面记:
- 当子树“暂时不能就绪”时
- React 可以先渲染 fallback
- 等待条件满足后再渲染真正内容
11.4 Suspense 为什么重要
它让“等待态”变成 React 渲染模型的一部分,而不是完全靠业务手写 if-else 管理。
11.5 useDeferredValue
用于把某个值的更新延后,让输入和重计算解耦。
11.6 React 18 原理答题关键词
- 自动批处理
- 可中断渲染
- 优先级调度
- 过渡更新
- Suspense
12. 事件系统与上下文更新
12.1 React 事件系统怎么理解
从使用层看,React 提供的是统一事件接口。
从原理角度,可以理解为:
- React 会做事件封装
- 事件分发和更新调度结合更紧密
- 这样有利于跨平台一致性和内部控制
12.2 为什么 React 要有自己的一层事件系统
理解角度:
- 统一不同浏览器行为
- 更好和 Fiber 更新流程协作
- 提供一致的开发体验
12.3 Context 更新怎么影响渲染
当 Provider 的 value 变化时,依赖这个 Context 的消费组件会重新参与更新。
所以使用 Context 时要注意:
- 不要把频繁变化的大对象直接整包传下去
- 必要时拆分多个 Context
- 让变化范围尽量局部
12.4 为什么 Context 容易带来性能问题
因为它的更新范围如果设计不好,会比较广。
所以面试里可以答:
- Context 适合中小规模共享数据
- 高频复杂全局状态不一定最适合 Context 直接硬扛
13. 常见源码面试题
13.1 为什么需要 Fiber
标准答法:
- 早期递归更新一旦开始不能中断
- 大树更新会长时间占用主线程
- Fiber 把更新拆成可中断、可恢复的工作单元
- 这样 React 才能实现更细粒度调度和并发能力
13.2 React Diff 为什么不是 O(n^3)
答:
- React 没采用通用树最优 Diff
- 它基于“不同类型生成不同子树”和“key 标识同层稳定节点”两个假设
- 只做同层比较,从而把复杂度控制在更可接受的范围
13.3 render 和 commit 区别
答:
- render 阶段负责计算新 Fiber 树和收集副作用
- commit 阶段负责真正提交 DOM 变更和执行副作用
- render 可中断,commit 不可中断
13.4 Hook 为什么不能条件调用
答:
- Hook 状态在 Fiber 上按调用顺序关联
- 条件调用会破坏顺序
- 后续 Hook 对应关系会错乱
13.5 useEffect 为什么不是同步执行
答:
useEffect属于提交后的后置副作用- 它不阻塞浏览器绘制
- 更适合请求、订阅、日志等副作用逻辑
13.6 key 的作用是什么
答:
- 用来标识同层节点身份
- 帮助 React 在列表更新中复用节点
- 减少错误移动和状态错位
13.7 React 18 并发渲染是什么
答:
- 不是多线程
- 是更新过程可被打断和按优先级调度
- 目的是提高用户交互流畅度
13.8 自动批处理是什么
答:
- React 会把同一批次中的多个更新合并处理
- 减少重复渲染
- React 18 覆盖场景更广
13.9 面试回答总模板
源码题建议按这个顺序回答:
- 先说它解决什么问题
- 再说核心机制
- 再说流程
- 最后说收益和代价
14. 8 小时源码学习规划
第 1 小时:搭地图
目标:
- 搞懂 React、React DOM、Reconciler、Scheduler 的角色
- 建立从 JSX 到 DOM 的主流程
必须会:
- Element 和 Fiber 区别
- render/commit 两阶段
第 2-3 小时:Fiber 架构
目标:
- 搞清楚为什么需要 Fiber
- 理解 Fiber 节点结构
- 理解 child/sibling/return 和 alternate
必须会:
- Fiber 是什么
- 双缓冲是什么
- 工作单元是什么
第 4 小时:调度与优先级
目标:
- 理解调度存在的意义
- 理解优先级
- 理解时间切片思想
必须会:
- 为什么需要打断更新
- Lane 在概念上解决什么问题
第 5 小时:Diff
目标:
- 吃透同层比较
- 理解 key 的意义
- 理解新增/删除/移动
必须会:
- 为什么只比较同层
- 为什么 key 不能乱用 index
第 6 小时:Hooks 原理
目标:
- 理解 Hook 链表
- 理解更新队列
- 理解 effect 执行时机
必须会:
- useState 为什么不能条件调用
- useEffect 为什么有 cleanup
第 7 小时:React 18 并发特性
目标:
- 理解自动批处理
- 理解 transition
- 理解 Suspense 在渲染模型中的意义
第 8 小时:复盘 + 面试表达
建议:
- 自己画出 Fiber 树关系图
- 自己讲一遍 render/commit
- 自己讲一遍 useState 原理
- 自己讲一遍 key 和 Diff
15. 一页速记总结
15.1 Fiber
- Fiber 是工作单元
- 支持可中断、可恢复、可调度
- 节点之间通过 child/sibling/return 连接
15.2 双缓冲
- current 是当前展示树
- workInProgress 是正在计算的新树
- commit 后 WIP 变 current
15.3 调度
- 更新有优先级
- React 能按优先级安排工作
- 时间切片让大任务分段执行
15.4 Diff
- 同层比较
- 类型不同直接替换
- 列表比较依赖 key
15.5 Hooks
- Hook 状态挂在 Fiber 上
- 靠调用顺序匹配
- useState 走更新队列
- useEffect 在 commit 后执行
15.6 React 18
- 自动批处理
- 并发渲染能力
- 过渡更新
- Suspense 增强
16. 背诵口诀
16.1 Fiber 口诀
Fiber 是工作单元,可打断可恢复,替代同步递归更新。
16.2 Diff 口诀
不同类型直接换,同层节点靠 key,比的是身份不是位置。
16.3 Hooks 口诀
Hook 挂 Fiber,顺序不能乱;state 进队列,effect 提交后再算。
16.4 React 18 口诀
批处理更主动,更新可分轻重,渲染可被打断,等待态有 Suspense。
17. 源码阅读路线
17.1 第一阶段:只看概念,不抠细节
先回答这几个问题:
- 为什么需要 Fiber
- current 和 workInProgress 是什么
- render 和 commit 分别做什么
- Hook 为什么不能条件调用
17.2 第二阶段:按主线看
推荐顺序:
- 先看整体更新流程
- 再看 Fiber 和双缓冲
- 再看 Diff
- 再看 Hooks
- 最后看调度与优先级
17.3 第三阶段:带着问题看源码
不要问“这行代码是什么意思”,而要问:
- 这一段在更新流程里属于哪一步
- 它解决的是哪个问题
- 如果没有它会怎样
17.4 阅读时最值得记录的东西
- 关键数据结构
- 关键流程函数
- 阶段划分
- 面试表达模板
17.5 最后给你的建议
学源码最怕陷入“细节海洋”。
真正高效的方式是:
- 先拿下概念主线
- 再记住几个关键流程
- 最后再把源码实现细节往主线里挂
你只要能稳定讲清这 5 个点,React 原理面试基本就稳了一大半:
- Fiber 为什么出现
- render/commit 为什么分阶段
- Diff 为什么只比较同层
- Hook 为什么靠顺序
- React 18 并发更新到底是什么