因为
React 靠 Hook 的调用顺序来匹配状态,顺序一变,状态就错位了。
这句话就是核心,不要一上来就讲 Fiber、链表、源码。面试官继续追问,再往下展开。
面试官继续追问后的展开
1. React 到底怎么"找状态"?
函数组件对应一个 Fiber,Fiber 上会挂着这个组件的 Hook 链表:
text
Fiber
↓
Hook1 → Hook2 → Hook3 → null
比如:
js
function App() {
const [name] = useState('张三');
const [age] = useState(18);
useEffect(() => {}, []);
}
React 实际上会形成:
text
Hook1 → useState(name)
↓
Hook2 → useState(age)
↓
Hook3 → useEffect
下一次 render 时,React 还是按照 Hook 的调用顺序去匹配:
text
第 1 次 Hook 调用 → Hook1
第 2 次 Hook 调用 → Hook2
第 3 次 Hook 调用 → Hook3
所以 React 并不是靠:
js
name
age
这些变量名找状态,而是靠Hook 的位置和顺序。
2. 为什么 if 会出问题?
例如:
js
function App({ isLogin }) {
if (isLogin) {
useState('张三');
}
useState(18);
}
第一次:
text
isLogin = true
Hook1 → 张三
Hook2 → 18
下一次:
text
isLogin = false
Hook1 → 18
问题就在这里:
text
第一次:
第1个 Hook → name
第2个 Hook → age
第二次:
第1个 Hook → age
age 本来是第二个 Hook,现在突然变成第一个 Hook 了。
于是 React 按位置拿到的状态就错位了。
3. 所以真正禁止的是什么?
不是简单的:
"Hooks 不能写在
if里。"
而是:
每次 render,Hooks 的调用顺序必须稳定。
所以这些写法都可能破坏这个规则:
js
if (condition) {
useState();
}
js
for (...) {
useEffect();
}
js
while (...) {
useMemo();
}
甚至:
js
function foo() {
useState();
}
然后:
js
if (condition) {
foo();
}
本质都是一个问题:
text
Hook 调用顺序可能变化
↓
Hook 对不上
↓
状态 / effect / memo 等数据错位
4. 为什么循环也不能写?
例如:
js
for (let i = 0; i < count; i++) {
useState(i);
}
第一次:
text
count = 3
Hook1
Hook2
Hook3
下一次:
text
count = 2
Hook1
Hook2
Hook 的调用数量变了,后面的 Hook 位置自然就变了。
这里有个容易说错的地方
如果你写:
js
for (let i = 0; i < 3; i++) {
useState(i);
}
而且每次 render 都永远执行 3 次,单纯从"顺序是否稳定"这个底层条件来说,确实不一定产生错位。所以面试时不要说:
"循环一定会导致 Hook 顺序错乱。"
更准确的是:
循环的迭代次数是动态的,很容易改变 Hook 的调用次数;React 因此统一要求 Hook 只能在顶层调用。
5. 为什么自定义 Hook 也不能放进分支?
例如:
js
function useUser() {
const [name] = useState('张三');
return name;
}
function App({ isLogin }) {
if (isLogin) {
useUser();
}
const [age] = useState(18);
}
很多人以为:
text
App Fiber
├── useUser
└── useState
其实不是。自定义 Hook 本身只是普通函数。
执行:
js
useUser();
实际上就是执行里面的:
js
useState();
所以最终还是:
text
App Fiber
↓
Hook1 → useUser 里的 useState
Hook2 → age
如果 isLogin 改变:
text
第一次:
Hook1 → name
Hook2 → age
第二次:
Hook1 → age
一样会错位。所以:
自定义 Hook 本身不占一个 Hook 位置,但它里面调用的 Hook 会占位置。
6. 自定义 Hook 的状态到底属于谁?
这是继续往深处追时很重要的一点。
属于调用这个自定义 Hook 的组件 Fiber。
js
function useCounter() {
const [count, setCount] = useState(0);
return [count, setCount];
}
如果:
js
function A() {
useCounter();
}
function B() {
useCounter();
}
并不是:
text
useCounter
↓
自己的 state
而是:
text
A Fiber
└── Hook → count
B Fiber
└── Hook → count
因为自定义 Hook 没有自己的 Fiber,也没有独立的 Hook 链表。
它只是:
把一组 Hook 调用封装起来。
7. 为什么 useEffect 里面不能再调用自定义 Hook?
例如:
js
useEffect(() => {
useUser(); // ❌
}, []);
因为 useEffect 的 callback 不是组件 render 的一部分。
真正的时间顺序是:
text
组件 render
↓
调用 useEffect()
↓
注册 effect
↓
render 结束
↓
之后执行 effect callback
↓
useUser()
↓
useState()
此时已经不在 React 当前这次 render 的 Hook 调用过程中。所以不是:
"自定义 Hook 不能在 useEffect 里用。"
而是:
React Hook 必须在组件或自定义 Hook 的 render 调用链里执行,不能等到 effect、事件回调等普通函数执行时再调用。
8. 为什么 React 要采用这种设计?
因为 Hook API 没有要求你显式传:
js
useState('name', '某个唯一ID')
React 需要知道:
text
这次 useState
到底对应上一次哪个 useState?
它采用的办法非常简单:
text
当前组件 Fiber
↓
Hook 链表
↓
第 1 个调用 → 第 1 个 Hook
第 2 个调用 → 第 2 个 Hook
第 3 个调用 → 第 3 个 Hook
所以整个规则的第一性原理就是:
text
Hook 顺序稳定
↓
位置稳定
↓
状态才能正确对应
反过来:
text
Hook 顺序变化
↓
位置变化
↓
状态错位
9. 最后用一个完整例子串起来
js
function useUser() {
const [name] = useState('张三');
return name;
}
function App({ isLogin }) {
const name = useUser();
const [age] = useState(18);
useEffect(() => {
console.log(name);
}, [name]);
return ...
}
React 看到的 Hook 顺序其实就是:
text
App Fiber
↓
Hook1 → useUser 里的 useState
Hook2 → App 里的 useState
Hook3 → App 里的 useEffect
下一次 render 必须还是:
text
Hook1 → useUser 里的 useState
Hook2 → App 里的 useState
Hook3 → App 里的 useEffect
如果你写成:
js
if (isLogin) {
useUser();
}
那么:
text
登录:
Hook1 → useUser
Hook2 → age
Hook3 → effect
未登录:
Hook1 → age
Hook2 → effect
从这里开始,整个 Hook 链就错位了。
最终满分回答
Hooks 不能写在分支、循环里,根本原因不是语法限制,而是 React 按调用顺序匹配 Hook。每个组件 Fiber 上都有一条 Hook 链表,每次 render 都按顺序取对应的 Hook;一旦分支或循环改变了 Hook 的调用次数或顺序,前后的 Hook 就对不上,状态、effect、memo 等数据就会错位。
这道题建议你最终记住三个层次
text
第一层:为什么不能写?
→ React 按调用顺序匹配 Hook。
第二层:顺序乱了会怎样?
→ Hook 对不上,状态等数据错位。
第三层:底层怎么实现?
→ Fiber.memoizedState 指向 Hook 链表,
render 时按顺序遍历和匹配。
第一句只说第一层。后面两层,等面试官追问再展开。