ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

React面试题:Hooks 为什么不能在分支和循环里面写?

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