这篇文章想解决 3 个常见问题:
- 为什么 Hook 不能写在 if 里?
- 为什么依赖数组漏了一个值就会出现"旧状态/旧闭包"?
- 为什么 setState 之后立刻拿到的还是旧值?
下面我们换个更直观的角度:
- Hooks 的数据到底"放哪儿了"?
- setState 之后为什么不会立刻变?
- 依赖数组漏写为什么会拿到"旧闭包"?
把这三点串起来,你就能把上面 3 个问题一次性想明白。
1. "副作用"到底是什么?(为什么需要 useEffect)
React 是基于 UI 驱动的前端框架,UI 是 props/state 的函数结果。理想情况下,渲染应该是纯函数:输入确定,输出也确定,不依赖和修改任何外部变量,这叫"没有副作用"。
但实际开发中经常要做这些事情:数据请求、定时器、DOM 操作等。这些都依赖或改变"组件外部"的世界,所以被称为副作用。
2. Hooks 的引出:函数组件怎么拥有状态与副作用能力
为了让函数组件也能拥有"状态"和"副作用处理能力",React 引入 Hooks。
比如 useState、useEffect 等 API,让函数组件也能维护内部状态,并在合适的时机处理请求、订阅、DOM 操作等副作用。
3. Hook 的数据存在哪?
3.1 Fiber 节点:函数组件的"档案袋"
在 React 内部,你可以把 Fiber 节点理解成"这个组件实例的身份证":组件每在页面里出现一次,就会有一个对应的 Fiber。
组件所有的 Hooks 数据(state、ref、effect 的一些信息等),都会跟着这个 Fiber 走,最关键的入口就是 memoizedState。
另外,Fiber 也是 React 用来做调度和更新的基本单位。跟我们这篇文章关系最大的几个字段就是:memoizedProps、memoizedState、updateQueue。
顺带说一句虚拟 DOM:它本质就是用普通 JS 对象描述"界面长什么样",React 会在合适的时机把这些变化真正同步到浏览器的 DOM 上。

比如下面这一段代码,展示 DOM 和虚拟 DOM 之间的对应关系:
jsx
<div id="app" class="container">
<p>hello</p>
</div>
jsx
{
type: 'div',
props: {
id: 'app',
className: 'container'
},
children: [
{ type: 'p', children: 'hello' }
]
}
3.2 memoizedState 是什么?它为什么是"链表"
memoizedState 本身是一个链表头 。链表中按固定顺序 存储所有 Hook 的状态:包括 useState 的值、useReducer 的 state、useRef、useEffect 的依赖等。
例如:
jsx
function Demo() {
const [count, setCount] = useState(0)
const [text, setText] = useState('abc')
return <div>{count} {text}</div>
}
React 内部会为每个 Hook 创建一个 hook 对象:
jsx
const hook1 = {
memoizedState: 0, // count 的值
next: hook2 // 指向第二个 hook
}
const hook2 = {
memoizedState: 'abc',// 第二个 useState 的缓存值
next: null // 链表尾部
}
然后把第一个 hook 挂载到 Fiber 节点:
jsx
fiberNode.memoizedState = hook1
最终形成:
jsx
fiberNode.memoizedState = {
memoizedState: 0,
next: {
memoizedState: 'abc',
next: null
}
}
所以:fiberNode.memoizedState 就是 Hook 链表的头结点。
3.3 为什么 Hooks 不能在 if / for / 嵌套函数里使用?
因为 React 是按顺序 ,从 memoizedState 链表从头往后依次取 Hook 的:
- 第 1 次调用 useState → 取链表第 1 个 hook
- 第 2 次调用 useState → 取链表第 2 个 hook
- 第 3 次调用 useEffect → 取链表第 3 个 hook
如果在 if 里写 Hook:
jsx
if (someCondition) {
const [count, setCount] = useState(0)
}
const [text, setText] = useState('abc')
- 第一次渲染:条件成立 → 链表顺序是
useState → useState - 第二次渲染:条件不成立 → 少执行一个 Hook,后面的 Hook 取到的"位置"就错了
**链表顺序对不上,状态就会串位。**所以 React 强制要求:所有 Hooks 必须在组件顶层,并按固定顺序执行。
4. updateQueue:更新任务先存哪?
为什么 setState 之后立刻读到的还是旧值呢?
核心点是:setState 不是"立即改 memoizedState",而是先把更新放进队列,等 React 调度到这一轮渲染时再统一计算。
4.1 updateQueue 介绍:
组件最终生效的状态会存在 memoizedState 里。
那在"状态更新还没真正计算、还没生效之前",这些更新任务存在哪?答案就是:updateQueue。
它的作用是:收集所有尚未处理的 state 更新,形成一个更新队列。
例如:
jsx
const [count, setCount] = useState(0)
setCount(1)
setCount(2)
setCount(3)
连续调用三次 setCount 并不会立即修改 memoizedState。
React 会先创建 3 个更新对象,放进 updateQueue 排队:
jsx
fiber.updateQueue = {
pending: {
action: 1,
next: {
action: 2,
next: {
action: 3,
next: ... // 环形链表,最后指回第一个
}
}
}
}
4.2 updateQueue 如何把更新跑起来:
updateQueue 自己不会"变更界面",它更像一个盒子:先把这次要改什么记下来。
当把更新对象加入 updateQueue 时:
- React 会把这个 fiber 标记为"有更新"。
- 根据更新优先级发起调度。
- 调度开始后进入 render 阶段:遍历 Fiber 树,从 updateQueue 取出更新,计算出最终新 state,保存到 memoizedState。
- React 会对比前后两次"界面应该长什么样",算出需要改动的那一小部分。
- commit 阶段修改 DOM,最终更新视图。

4.3 为什么 setState 后立刻打印还是旧值?
因为你"调用 setState 的那一刻",更新还只是进入了 updateQueue。
真正写入 memoizedState 并触发重新渲染,要等 React 在后续调度中跑完 render/commit。
另外,如果你的新状态依赖旧状态,建议用函数式更新,避免读到旧值:
jsx
setCount(c => c + 1)
5. 为什么依赖数组漏值会出现"旧状态/旧闭包"(解决:依赖数组问题)
关键点是:每次渲染都会产生一个新的闭包。
useEffect 如果依赖数组是空的,它只会在首次渲染时记录那次渲染产生的回调函数,后面的渲染不会替换掉它,于是回调里拿到的 state/props 就会"停留在第一次"。
例如:
jsx
useEffect(() => {
console.log(count)
}, [])
常见写法大概两种:
不写依赖数组(每次渲染后都会执行一次):
jsx
useEffect(() => {
// 组件初始化 + 每次更新都会执行
})
写依赖数组(依赖变了才执行):
jsx
useEffect(() => {
// count 变化时才执行
}, [count])
到这里,这几个常见疑问其实都能落到同一个答案:
- React 取 Hooks 状态,靠的是 Fiber 上那条 按顺序排好的 Hook 链表。
- 你调用 setState 的时候,React 先把"要怎么改"塞进 updateQueue,等这一轮调度真正跑起来再统一算。
- effect 回调里拿到的变量,本质上来自"某一次渲染生成的闭包",依赖数组写漏了,就可能一直用的是旧那次。
把这三点抓住,后面的学习才会行稳致远