React Hooks底层执行逻辑详解
React Hooks 在表面上看像普通的函数调用,背后却隐藏着一套复杂而高效的运行时机制。要理解 React Hooks 的底层执行逻辑,需要从 React 如何管理组件的状态与副作用入手。
🧠 一、React 为什么引入 Hooks?
在 class 组件中,状态逻辑分散、复用困难(通过 HOC、render props 实现)。Hooks 通过“函数组件 + 闭包 + 顺序调用”机制,解决了这些问题,让函数组件拥有“类”的能力。
🔍 二、Hooks 的核心机制:调用顺序与数组栈
✅ 核心思想:
React 内部为每个组件实例维护一个 “Hook 调用栈”(本质上是一个数组),Hooks 的执行顺序严格依赖于你在函数中书写的顺序。
举个例子:
function MyComponent() {
const [count, setCount] = useState(0) // 第一个 Hook(索引 0)
const [name, setName] = useState('') // 第二个 Hook(索引 1)
}
React 内部逻辑可以抽象为伪代码:
let hookIndex = 0
const hooks = []
function useState(initialValue) {
const currentIndex = hookIndex
if (isMount) {
hooks[currentIndex] = typeof initialValue === 'function' ? initialValue() : initialValue
}
const setState = (newValue) => {
hooks[currentIndex] = newValue
triggerComponentUpdate()
}
hookIndex++
return [hooks[currentIndex], setState]
}
✔️ 所以:每次重新渲染组件时,React 都会以相同顺序重新执行
useXXX()
,来匹配状态数组中的值。
🔧 三、Hooks 的执行流程(React 内部机制)
🧩 每次组件渲染时:
当前组件会进入 render phase
React 初始化
fiberNode.memoizedState
(Hook 存储区)每次调用一个 Hook(如
useState
、useEffect
):- React 用当前的
hookIndex
取出对应位置的值 - 更新完后
hookIndex++
- React 用当前的
所有 Hook 调用完毕后:
memoizedState
就是这个组件的 Hook 状态链- React 将其挂在 fiber 树上,供下一次渲染使用
⚙️ 四、不同 Hook 的底层行为
1️⃣ useState
- 在首次渲染时保存初始值
- 后续调用 setState 会触发组件更新,并保留新值在状态数组中
2️⃣ useEffect
- 注册副作用及清理函数
- 保存依赖数组,用于下一次渲染对比
- 在 commit 阶段执行副作用
3️⃣ useRef
- 保存一个
{ current: ... }
对象 - 是在 Hook 数组中创建的稳定引用,不会重新创建
4️⃣ useMemo
/ useCallback
- 保存返回值或函数引用
- 对比依赖数组决定是否复用旧值
📊 五、Hook 状态存储结构:Fiber Node
每个组件的所有 Hook 状态,都挂载在它自己的 Fiber Node 上的 memoizedState
字段中,它实际上是一个单链表结构。
FunctionComponentFiberNode
└── memoizedState --> HookState(useState)
└── next --> HookState(useEffect)
└── next --> ...
这个结构意味着:
- 每个 Hook 对应链表中的一个节点
- 遍历顺序必须与调用顺序保持一致,否则状态将错位(所以 Hooks 不能写在条件语句中)
🚨 六、为什么 Hooks 不能放在 if 语句里?
因为状态顺序是通过“调用顺序 + 索引”来维护的。如果你这样写:
if (someCondition) {
useState(...) // ⚠️ 不一定会调用
}
那么下一次渲染时,Hook 的数量或顺序可能变化,导致状态错乱(取到了别的 Hook 的状态)!
React 会通过内部开发环境检查这些不规范用法。
🧬 七、Hook 触发组件更新的机制
当调用 setState(newVal)
:
- 会创建一个更新对象,加入到组件的更新队列
- 标记当前 Fiber 为“需要更新”
- 触发 React Scheduler 安排任务(基于优先级)
- 进入 render phase,重新执行组件函数(重新执行所有 Hooks)
- 比较 Fiber 树 → 更新 DOM
🧪 八、一个模拟实现:useState
let hookStates = []
let hookIndex = 0
function useState(initialValue) {
const currentIndex = hookIndex
hookStates[currentIndex] = hookStates[currentIndex] || initialValue
function setState(newVal) {
hookStates[currentIndex] = newVal
render()
}
hookIndex++
return [hookStates[currentIndex], setState]
}
function render() {
hookIndex = 0
ReactDOM.render(<App />, document.getElementById('root'))
}
✅ 九、总结:Hooks 底层关键点
机制 | 说明 |
---|---|
顺序执行原则 | Hooks 必须按照一致顺序调用 |
状态数组/链表结构 | 每个 Hook 都在 Fiber 节点的状态链表中占一项 |
更新触发原理 | setState 会触发组件的调度和重新渲染 |
Hooks 本质 | React 自定义的一套状态和副作用管理系统,依赖“闭包 + 引用 + 顺序”维护状态 |
自定义Hook
自定义 Hooks 是现代 React 中最重要的模式之一,用于在函数组件之间复用逻辑。它是一种在组件外提取公共逻辑的方式,优雅地替代了以前 class 组件中用 HOC 或 render props 的做法。
🧠 一、自定义 Hook 是什么?
自定义 Hook 就是一个以 use
开头的 JavaScript 函数,内部可以调用其他 Hooks(如 useState、useEffect、useContext 等)。
它并不需要拥有特殊的语法,而是遵守命名规范(以 use
开头)和 Hook 规则(只能在顶层和 React 函数组件中调用)。
🔧 二、基础语法结构
function useMyHook() {
const [state, setState] = useState(initialValue)
// 可以包含副作用
useEffect(() => {
// 例如订阅数据
return () => {
// 清理操作
}
}, [])
return { state, setState }
}
使用方法:
function MyComponent() {
const { state, setState } = useMyHook()
return <div>{state}</div>
}
🛠️ 三、常见自定义 Hook 示例
1️⃣ useWindowSize:监听窗口大小
import { useState, useEffect } from 'react'
function useWindowSize() {
const [size, setSize] = useState({ width: window.innerWidth, height: window.innerHeight })
useEffect(() => {
const handleResize = () => {
setSize({ width: window.innerWidth, height: window.innerHeight })
}
window.addEventListener('resize', handleResize)
return () => window.removeEventListener('resize', handleResize)
}, [])
return size
}
2️⃣ useFetch:封装数据请求逻辑
import { useState, useEffect } from 'react'
function useFetch(url) {
const [data, setData] = useState(null)
const [loading, setLoading] = useState(true)
useEffect(() => {
setLoading(true)
fetch(url)
.then(res => res.json())
.then(data => {
setData(data)
setLoading(false)
})
}, [url])
return { data, loading }
}
使用:
const { data, loading } = useFetch('/api/user')
🧩 四、自定义 Hook 的使用场景
场景 | Hook 示例 |
---|---|
状态共享 | useForm , useTheme , useAuth |
业务逻辑抽象 | usePagination , useLogin |
操作 DOM | useScroll , useFocus |
事件/副作用封装 | useOnlineStatus , useDebounce |
状态机器或流程控制 | useWizard , useStepper |
🚨 五、自定义 Hook 的注意事项
1. 必须以 use
开头
否则 React 无法检测是否遵守 Hook 的规则。
2. 遵循 Hook 规则(顶层调用)
- 不要在 if、for、事件中调用 Hook
- Hook 必须在函数组件或另一个 Hook 中使用
Hooks 是
按调用顺序管理状态
的,调用顺序必须稳定
,执行上下文必须是“可追踪”的(函数组件或 Hook)
,否则 React 无法正确管理它们。
3. 避免副作用重复(加依赖)
useEffect(() => {
// fetch or something
}, [url]) // 依赖项必须写全
✅ 六、自定义 Hook vs HOC/Render Props
特性 | 自定义 Hook | HOC | Render Props |
---|---|---|---|
写法简洁 | ✅ 非常简洁 | ❌ 会嵌套 | ❌ 需要 props 回调 |
类型推导好 | ✅ 原生支持 | ❌ props 类型麻烦 | ❌ |
可组合性 | ✅ 任意组合 | ✅ | ✅ |
推荐程度 | ✅ 推荐使用 | ⛔ 旧方式(不推荐) | ⛔(不推荐) |
📦 七、如何组织自定义 Hook?
建议创建一个 hooks/
文件夹,每个 Hook 一个文件。例如:
src/
├── hooks/
│ ├── useAuth.js
│ ├── useFetch.js
│ └── useWindowSize.js
✨ 总结
你应该使用自定义 Hook 的时候: |
---|
有重复逻辑(如监听、请求、共享状态等) |
逻辑独立但不涉及 UI |
想增强可维护性和可测试性 |
如果你告诉我你项目中具体遇到的重复逻辑或问题,我可以帮你定制一个适合你项目的自定义 Hook 模板。是否要尝试?
Fiber&Scheduler
Fiber 架构 + Scheduler 是 React 实现 并发渲染 的核心。它们分别负责「任务的组织与执行逻辑」和「任务的调度与执行时机」。
🧠 一、React Fiber 架构详解
Fiber 是 React 16 引入的全新架构,用来替代旧的 Stack Reconciler。它的设计目标是:
- 任务可被拆分(增量渲染)
- 渲染可中断、恢复、重用
- 支持并发调度
📦 1. Fiber 是什么?
一个 Fiber 就是一个组件的工作单元(Work Unit),它是一个 JavaScript 对象,描述了:
type FiberNode = {
type: Function | string;
key: string | null;
child: FiberNode | null;
sibling: FiberNode | null;
return: FiberNode | null; // 父节点
stateNode: any; // 组件实例
memoizedProps: any;
memoizedState: any;
alternate: FiberNode | null; // 双缓冲
flags: number; // 副作用标记
...
}
🧭 2. Fiber Tree 构建过程
每次更新时,React 会创建一棵新的 Fiber 树(work-in-progress tree),由当前树(current tree)复制并修改。
- 双缓冲机制:
current
&workInProgress
交替使用 - 每个更新任务递归生成 Fiber 节点,构成完整 Fiber 树
🔁 3. Fiber 的工作循环(核心阶段)
🔨 Reconciliation(协调阶段)
- 调用组件函数(或类的 render)生成新的虚拟 DOM
- 比较新旧 Fiber 树,标记哪些节点需要变更
- 构建 “Effect List”(副作用链表)
🚀 Commit(提交阶段)
- 根据 Effect List 执行真实的 DOM 操作(插入/删除/更新)
- 不可被打断,必须同步完成
🕹️ 二、Scheduler 调度器详解
Scheduler 是 React 的调度核心,负责 管理 Fiber 的执行时机与优先级,使 React 拥有「可中断」和「可恢复」的能力。
📋 1. 核心能力
- 管理任务队列
- 计算任务优先级(Lanes)
- 根据浏览器空闲时间切片执行
- 决定是否让出主线程(
shouldYield
)
⏳ 2. 时间切片(Time Slicing)
while (work && !shouldYield()) {
work = performUnitOfWork(work);
}
- 每次只做一小部分工作(一个 Fiber 节点)
- 超出时间阈值就让出执行权,避免卡住主线程
📊 3. 优先级系统(Lanes)
React 引入「Lanes(车道)」作为多优先级调度方案:
const NoLane = 0b00000
const SyncLane = 0b00001 // 同步优先级
const InputContinuousLane = 0b00010 // 用户输入优先
const DefaultLane = 0b00100
const IdleLane = 0b10000 // 最低优先
React 会根据任务类型分配 Lane,调度器根据当前空闲情况,调度最高优先的任务先执行。
⚙️ 三、Fiber + Scheduler 协同流程图
用户触发更新(如点击按钮)
↓
Scheduler 收到任务,放入任务队列
↓
根据 Lane 决定优先级 & 是否立即执行
↓
Fiber Tree 被构建(协调阶段)
↓
每个 Fiber 任务以时间切片形式执行
↓
中途检查 shouldYield(),必要时中断
↓
所有 Fiber 构建完成,进入 commit 阶段
↓
一次性提交 DOM 修改(同步执行)
🧩 总结:Fiber & Scheduler 分工
功能 | Fiber | Scheduler |
---|---|---|
结构 | 描述每个组件的渲染任务 | 管理任务执行的优先级和时机 |
拆分任务 | 将渲染工作拆成一个个 Fiber 节点 | 将任务切片处理,防止阻塞主线程 |
可中断机制 | 通过时间切片,暂停和恢复渲染 | 决定何时中断、恢复和重新调度任务 |
优先级处理 | 每个 Fiber 带有优先级(lane) | 任务队列按优先级排序 |
如果你需要我通过可视化图解、源码级解析(如 Scheduler 源码调度流程),我也可以帮你补充。是否继续?