想象你是一家生意火爆的餐厅后厨的总厨师长。每个灶台对应一个团队,每天要接待几十桌客人——每一桌点单后,厨师都要把整套流程重新走一遍:洗菜、切菜、下锅、装盘。这正是 React 组件的工作方式:每次状态变化,整个组件函数都会重新执行一遍。但问题来了:做完一道菜,食材还剩多少?上次的火候是多少?总不能每次都从零开始。于是 React 给每个“灶台”配备了一套 Hooks 工具墙——食材储物柜(useState)、打烊清洁清单(useEffect)、提前备好的半成品柜(useMemo)。这些工具都挂在固定位置,React 靠“挂钩的顺序”来识别它们,就像后厨墙上按顺序挂着的炒勺、漏勺、夹子,永远不会拿错。同时,每次烹饪时厨师会在便签上记下当时的食材量和火候——这就是闭包。一旦菜品出锅,便签上的数字就定格了,你再拿起它想看“最新火候”,看到的还是老数字。理解了“挂钩墙 + 便签”的运行逻辑,你就真正读懂了 Hooks 的底层原理。
掌握 Hooks 的闭包机制与链表实现,能从“会用 Hook”进阶到“理解 Hook”,在团队协作中精准定位闭包陷阱、依赖遗漏等疑难 bug,写出健壮可维护的 React 组件。
目录
可视化演示
可视化展示useState状态管理、useEffect副作用处理和useMemo性能优化的工作机制。
本节配有交互动效(进入视口后自动加载,约 3KB)。
useState状态管理 闭包捕获状态值 状态变更触发重渲染 setState → 调度更新 → 重渲染
Hooks 初识:后厨的工具革命
在 React 16.8 之前,函数组件就像后厨里的“传菜员”——只负责把菜端出去,自己没有灶台、没有储物柜,稍微复杂一点的菜都得请“全职大厨”(class 组件)出马。class 组件有完整的生命周期和状态管理,但写起来又长又绕,this 指向问题更是让新手厨师头疼。React 团队于是在 16.8 版本引入了一套新工具——Hooks,让函数组件也能拥有“自己的灶台”。
Hooks 的英文原意是“钩子”,形象地描绘了它的作用:把函数组件“钩”在 React 的核心机制上。你可以把 useState 想象成一个食材储物柜,把 useEffect 想象成打烊后的清洁清单,把 useMemo 想象成提前备好的半成品。有了这三样法宝,函数组件就能独立掌勺。
既然是工具,就有使用规矩。Hooks 的黄金法则是:只能在函数组件顶层调用,不能放在循环、条件语句或嵌套函数里。因为 React 内部并没有给每个 Hook 绑定名字,它靠的是“这次渲染时调用顺序”来匹配数据——就像挂钩墙上的工具,第一个挂钩永远挂炒勺,第二个永远挂漏勺。你要是哪天心血来潮把它换个位置,React 就懵了。
理解了这套“工具墙”的逻辑,你就掌握了 Hooks 的第一块拼图:它们不是魔法,而是挂在固定位置的标准化工具。接下来我们看看,为什么每位厨师手边的“便签”——闭包——会成为新手最容易踩的坑。
- 组件函数 f它是“菜谱”,React 每次渲染都照它重新做一遍
- props 配料单父组件从外面传进来的“点单信息”,组件自己不能改
- state 库存组件内部用 useState 保存的“食材余量”,可以随更新变化
- hooks 工具墙挂载在当前组件上的所有 Hook 数据(状态、副作用、缓存)
- UI 出锅装盘函数返回的界面描述,交给 React 渲染到屏幕
闭包机制:定格在瞬间的便签
后厨有个习惯:每道菜下锅前,厨师会在便签上记下当时的食材量、火候和调料配比。这份便签一旦写好,就不会再改。哪怕五分钟后的菜换了新火候,你拿起刚才那张便签,看到的仍然是“当时”的记录。JavaScript 里的闭包,就是这种“定格便签”。
React 组件每次渲染,函数体都会重新执行一遍,每次执行都会创建一批全新的变量。如果一个函数(比如 setTimeout 的回调、事件处理器、useEffect 里的函数)是在第 t 次渲染时被创建出来的,那它捕获的,就是第 t 次渲染时的变量值——不是最新的,而是“诞生那一刻”的。
这就是著名的“闭包陷阱”(Stale Closure)。经典场景:在 useEffect 里用 setInterval 读 state,发现永远读到初始值。原因就在这——setInterval 回调是首次渲染时的闭包,它锁死的 count 是 0,之后组件再怎么更新,这张“旧便签”都不会被改写。解决办法是把 count 放进依赖数组让回调重建,或者用 useRef 保存一份“可写的实时值”。
闭包不是 bug,它是 JavaScript 语言的基本机制。理解了“便签一旦写下就定格”,你就能解释 React 里那些“读到旧值”的诡异现象——不是框架出错,而是你手里拿着一张旧菜单。
- C_t 闭包第 t 次渲染时创建的“便签快照”
- s_t 状态快照那份渲染时 useState 里的值
- p_t 参数快照那份渲染时收到的 props
- 定格不可变闭包锁死当时的值,之后组件更新也不会改写它,所以旧回调永远读旧值
Hook 链表:后厨的挂钩墙
现在来看 React 内部是怎么管理这些工具的。想象后厨有一面挂钩墙,墙上从左到右按顺序挂着炒勺、漏勺、夹子。每过一次营业周期,厨师都要按顺序把每个工具拿下来用一遍。React 的每个组件也有这样一面“墙”,只不过它挂的不是厨具,而是一个个 Hook 的数据。
这面墙在 React 内部对应的数据结构,是挂在 Fiber 节点上的一条单向链表。Fiber 是 React 为每个组件建立的一份“内部档案”,记录了这个组件的类型、状态、更新任务等一切信息。而组件调用的所有 Hook,就按调用顺序一个个串在这条链表上。
关键字段是 fiber.memoizedState——它指向链表头,也就是第一个 Hook。每个 Hook 节点通过 next 指针指向下一个,形成 h1 → h2 → h3 → … 的单向链表。当组件重新渲染时,React 从头开始遍历链表,第 i 次调用 Hook 就取第 i 个节点。所以 Hook 的调用顺序必须保持稳定:如果某次渲染你少调了一个 Hook,后面的全部错位,数据就“张冠李戴”了。
这面挂钩墙解释了所有 Hooks 规则的根本原因。为什么不能写在 if 里?因为 if 可能让某次渲染少挂一个工具,整个墙的顺序就乱了。理解链表,你就理解了规则的必然性。
- fiber 档案卡React 内部为每个组件维护的节点,存着组件的一切数据
- memoizedState 入口指向第一个 Hook 的指针,好比挂钩墙的起点
- h_i 挂钩节点第 i 个 Hook 的存储单元,包含类型、值、依赖等信息
- next 指针每个节点握住下一个节点的地址,把一串 Hook 串成链表
useState:食材储物柜
后厨灶台旁有一个储物柜,里面的食材余量能被“记下来”。就算你转身接待完十桌客人再回来,柜子里的食材还是你上次登记的量。useState 就是这个储物柜。
useState(initialValue) 的用法是:传一个初始值,得到 [当前值,更新函数]。比如 const [count, setCount] = useState(0)。第一次渲染时,React 在 Hook 链表上给这个 useState 分配一个节点,存入初始值 0。之后每次渲染,React 都从链表上取出这个节点的当前值返回给你。
调用 setCount 时,React 并没有立刻改写 count 的值——它只是把一个“更新动作”塞进了这个 Hook 节点的更新队列。等下一次渲染开始,React 从队列里依次取出更新动作,逐个执行,算出最终的新状态。这个队列可以看成一条流水线:初始值是 s0,第一个更新把 s0 变成 s1,第二个把 s1 变成 s2,最后的 s_n 才是本次渲染的状态。这就是为什么连续多次 setState 不会立刻反映在变量上——它们都在排队。
理解更新队列,你就知道了 React 的“记账式”工作方式:不当场改食材,而是记在账本上,统一对账。所以别指望 setCount 之后立刻读 count 能拿到新值——账还没对完呢。
- s_0 初始库存本次渲染开始时,useState 从链表节点上读到的旧状态
- U_i 排队更新第 i 个排队中的更新,可以是新值,也可以是一个“接收旧值返回新值”的函数
- 复合符号把多个更新“串起来按顺序执行”,一个的输出是下一个的输入
- s_next 最终状态全部更新执行完后的新状态,本次渲染使用它
useEffect:打烊后的清洁清单
后厨每天打烊后,厨师长会拿出一份清洁清单:检查冰箱、清洗灶台、关掉燃气。这份清单绝不会在营业高峰期插队执行——它一定等到所有菜都上完(页面渲染完毕),才会逐项进行。useEffect 干的就是这份“打烊后的活”。
useEffect(callback, deps) 接收一个副作用函数和一个依赖数组。副作用指的是“渲染之外”的事:请求数据、订阅事件、手动改 DOM、设置定时器等。React 保证:只要依赖数组里的值没变,副作用在本次渲染完成后就不会重复执行。
执行时机很关键——useEffect 中的回调是在浏览器完成绘制(paint)之后才执行的,不会阻塞用户看到页面。依赖数组就像清洁清单上的“检查条件”:哪一项变了,对应的任务才需要重做。如果传空数组 [],表示“这些活只在开业那天做一次”(组件挂载后执行一次)。如果完全不传依赖数组,那每次渲染后都会执行。另外,如果副作用里返回一个函数,它就是“清理函数”,会在下次副作用执行前或组件卸载时被调用——相当于“打扫完把工具归位”。
把 useEffect 想象成打烊后的例行检查,你就能记住三个要点:时机在绘制之后、依赖决定要不要做、返回值负责善后。这也是面试里最喜欢考的三件事。
- deps 检查清单第二个参数传入的依赖数组,列出副作用依赖的变量
- 逐项比较用 Object.is 比较本次与上次的每一项依赖是否相同
- E=1 开工有新变化就执行副作用(订阅、请求、改 DOM)
- E=0 跳过新旧依赖一模一样就直接跳过本次副作用
useMemo:提前备好的半成品
后厨里有个师傅,切土豆丝特别慢。聪明的做法是:趁客人少的时候把所有土豆丝切好,装进保鲜盒。客人点菜时,直接抓一把下锅,不用现场再切。useMemo 就是这个“保鲜盒”。
useMemo(() => compute(), deps) 接收一个计算函数和依赖数组。首次渲染时,React 执行计算函数,把结果存起来;后续渲染时,如果依赖没变,就直接返回缓存结果,不再执行那个昂贵的计算。
它适合的场景是“计算开销大但输入很少变”的情况,比如对大数组做过滤排序、复杂的数据聚合。但要注意:useMemo 不是免费的,React 每次渲染都要把依赖数组逐项比较一遍,这个比较本身也有成本。如果计算只要几毫秒,那缓存省下的时间可能还不够比较开销。所以别滥用,只把它留给真正的“硬骨头”。
备菜是个好习惯,但也没必要把每根葱都提前切好。useMemo 的哲学是“把力气花在值得的地方”——这是 React 性能优化的真谛。
- R 返回结果组件里拿到的计算结果
- f(d) 昂贵菜谱传入的计算函数,做的是又重又慢的活
- cache 半成品上次算好存在“保鲜盒”里的结果,直接拿出来用,不重算
- d 依赖判断只有依赖变化才“重新下锅”,否则“原样上菜”
依赖管理:核对清单的艺术
厨师长最头疼的事,就是清洁清单写错了:要么漏写了某扇窗户,导致永远没人擦;要么多写了某个不存在的柜子,导致每天白跑一趟。依赖数组的玄机,也是如此。
React 判断依赖是否变化的算法,是 Object.is——一种类似 === 的严格相等比较。它会把新旧依赖数组逐项拿出来比:只要有一项不相等,就认为“依赖变了”,需要重新执行副作用或重算缓存。
常见的两类问题:一是依赖遗漏——副作用里用了某个变量却没写进依赖数组,导致闭包读到旧值,这就是“漏写窗户”;二是依赖多余——把组件里根本没用的变量写进去,导致效果频繁触发,这就是“多写柜子”。React 官方提供了 eslint-plugin-react-hooks 的 exhaustive-deps 规则来自动检查这两类问题。面对复杂逻辑,还可以把一组相关的状态和副作用封装成自定义 Hook,让依赖关系更清晰。
好厨师最懂“清单要不多不少刚刚好”。写依赖数组时,多问自己一句:这个副作用究竟读了哪些变量?答案就是你的清单。
- 存在量词只要“有一个”依赖变化,就算整组变化,不必全部不同
- 下标 i依赖数组里的第 i 项
- Object.isReact 判断两个值是否相同的算法,类似 === 但更严格
- changed 结论只要有任意一项不同,就触发副作用或重算缓存
底层架构:订单看板背后的引擎
一间成熟的后厨,会有一个总调度室,墙上挂着一块订单看板:每道菜做到哪个步骤,一目了然。React 的 Fiber 架构就是这块“订单看板”——它把渲染拆成可以随时暂停、恢复的小步骤,让 React 知道“活干到哪了”。
React 组件从状态变化到页面更新,要经历两个阶段:渲染阶段(Render Phase)和提交阶段(Commit Phase)。渲染阶段里,React 执行组件函数,计算新的界面长什么样,并对比新旧虚拟 DOM 找出差异;提交阶段才把差异真正写到浏览器里。Hooks 的数据,正是在渲染阶段从链表上读取、在提交阶段才体现到页面上。
为了方便中途“插队”和暂停,React 会在内存里同时维护两棵 Fiber 树:一棵是当前屏幕上的 current 树,一棵是正在构建的 workInProgress 树。渲染完成后,把 workInProgress 树切换为新的 current 树,这个过程叫“双缓存”。这也解释了为什么 useEffect 不在渲染阶段执行——渲染阶段可能被打断,副作用必须在提交后“尘埃落定”再执行,否则容易重复或错乱。
这块订单看板,把所有“烹饪动作”拆解成可追踪的步骤,是 React 高性能的秘密。理解了 Render/Commit 两个阶段,你就从“会用 Hooks”进阶到了“懂 React 的心跳”。
- s 渲染快照本次渲染使用的状态与 props
- render 构建菜单执行组件函数,产出新的 UI 描述(虚拟 DOM)
- commit 上菜把变化提交给浏览器,真正修改真实 DOM
- DOM_new 成品用户最终在页面上看到的新界面
AI 应用场景
数据大屏:状态与缓存的分工
阶段:前端组件状态管理
解决的问题:数据大屏上几十个图表共享筛选条件,传统写法每次筛选变化都全量重算,页面卡顿。用 useState 统一管理筛选条件,用 useMemo 按依赖缓存聚合结果,把万级数据量的重算从“每次输入都触发”优化为“仅条件变化时触发”。
具体案例:某电商后台的销售大屏,时间段选择器、地区下拉、品类多选三个筛选项用 useState 存储;汇总数据用 useMemo 依赖这三个状态计算。用户拖动时间范围时,组件不会一帧一帧地重算柱状图数据,只有松开鼠标确认新范围后才重算一次。
实时监控页面的订阅管理
阶段:副作用管理与数据订阅
解决的问题:轮询接口、WebSocket 连接等持续性任务,传统写法容易造成连接泄漏——组件卸载了还在发请求。useEffect 的清理函数可以完美解决连接的生命周期问题。
具体案例:运维监控面板用 useEffect 建立 WebSocket 连接,依赖数组 [roomId] 控制“房间切换时自动断开旧连接并建立新连接”;返回的清理函数在组件卸载时关闭连接,避免内存泄漏。这也正是 AI 推理任务进度轮询页面的标准写法。
组件库中的自定义 Hook 封装
阶段:逻辑抽象与复用
解决的问题:多个组件都要“请求数据 + 加载态 + 错误态”,每个组件重复写一遍 useEffect 很痛苦。封装成自定义 Hook,一处编写、处处复用。
具体案例:团队封装 useFetch 自定义 Hook,内部用 useState 管理 data/loading/error,用 useEffect 发起请求,对外只暴露三个值和 refetch 方法。AI 聊天机器人前端用它拉取模型列表、历史会话,三个页面共用一份逻辑。
Python 演示
class HookNode: """链表中的一个 Hook 节点:保存值、更新队列和 next 指针""" def __init__(self, kind, value): self.kind = kind # 'useState' 或 'useMemo' self.value = value # 当前存的值 self.next = None # 指向下一个 Hook self.queue = [] # useState 的更新队列 class ComponentFiber: """模拟 React Fiber 节点:用单向链表保存组件上的所有 Hook""" def __init__(self): self.head = None self.tail = None def mount_hook(self, kind, value): """首次渲染时挂载 Hook:追加到链表尾部""" hook = HookNode(kind, value) if self.head is None: self.head = hook else: self.tail.next = hook self.tail = hook return hook def match_hook(self, index): """重新渲染时按索引匹配链表上的第 index 个 Hook""" cur = self.head for _ in range(index): cur = cur.next return cur def render(self): """模拟一次渲染:把每个 Hook 的更新队列依次执行""" cur = self.head values = [] while cur is not None: while cur.queue: updater = cur.queue.pop(0) cur.value = updater(cur.value) values.append(cur.value) cur = cur.next return values # 首次渲染:函数组件按固定顺序挂载两个 Hookfiber = ComponentFiber()state_hook = fiber.mount_hook('useState', 0)memo_hook = fiber.mount_hook('useMemo', 100) # 用户连续点击两次:两个更新动作排入队列,此时状态并未改变state_hook.queue.append(lambda s: s + 1)state_hook.queue.append(lambda s: s + 1) # 重新渲染:靠索引找回同一个 Hook,批量执行队列state_hook = fiber.match_hook(0)memo_hook = fiber.match_hook(1) state_value, memo_value = fiber.render()print('useState 最终值 =', state_value)print('useMemo 缓存值 =', memo_value)
self.tail.next = hook把新 Hook 追加到链表尾部,就像把新工具挂到挂钩墙最右边。fiber.mount_hook('useState', 0)首次渲染时按固定顺序挂载 Hook,React 会记住这个顺序。state_hook.queue.append(lambda s: s + 1)调用 setState 不会立刻改值,只是把更新动作排进队列。fiber.match_hook(0)重新渲染时靠索引找回同一个 Hook,顺序一旦变化就会错位。fiber.render()渲染时批量执行更新队列,依次加工算出最终状态。
常见误区
误区我明明 setCount 了,为什么在 setTimeout 回调里读到的 count 还是旧值?React 是不是有 bug?
正解这不是 bug,是闭包机制的正常表现。每次渲染都创建独立的快照,setTimeout 回调是当初那次渲染创建的“便签”,它锁死的是当时的值。组件重新渲染后,旧回调依然握着旧快照。想读最新值,可以把值放进依赖数组让回调重建,或用 useRef 保存一个始终指向最新值的可变引用。就像打烊后想查最新食材量,得去看新账本而不是旧便签。
为何常见:初学者的心智模型默认“变量是活的、会同步更新”,而闭包恰恰相反——它冻结捕获时刻的值,违反直觉。
误区反正不知道依赖怎么写,不写依赖数组,回调每次渲染都跑,功能还能正常实现。
正解不写依赖数组会让副作用每次渲染后都执行,既浪费性能又容易引发无限循环;如果回调里调用了 setState,就会陷入“渲染→副作用→setState→再渲染”的死循环。写 [] 表示只在挂载后执行一次,写了具体依赖则按需执行。依赖数组不是可选项,它是副作用执行的“触发条件说明书”。
为何常见:小 demo 中每次渲染执行副作用也能正常工作,让人误以为依赖数组可有可无,直到生产环境遇到性能问题和死循环才醒悟。
误区只要把计算包一层 useMemo,组件就快了;多包一层总没坏处。
正解useMemo 本身有代价——每次渲染都要比较依赖数组,比较也需要时间。如果计算本身很轻(几毫秒内),缓存省下的时间可能还不如比较开销大,反而更慢。正确做法是先测量,找到真正的性能瓶颈,再针对高频且昂贵的计算用 useMemo。备菜只备客人常点的硬菜,而不是把所有食材都切一遍。
为何常见:性能优化存在“没有测量就优化”的误区,加上对 useMemo 的营销式理解,导致滥用。
误区把 useState 写在 if 里面,条件成立时用,不成立就不用,挺灵活的。
正解React 在内部靠 Hook 链表存储数据,组件渲染时按调用顺序逐一匹配。一旦条件导致某次渲染少调用了一个 Hook,后面所有 Hook 的索引全部错位,React 会把上一个 Hook 的数据塞给下一个 Hook,导致状态错乱甚至程序崩溃。所以 Hooks 只能在顶层调用——挂钩墙的位置是钉死的。
为何常见:很多 Hook 看起来是普通函数调用,容易让人忽略它的底层是按位置存储的链表,条件调用的危害不会立刻暴露,但会在某个边缘条件触发时炸出来。
误区依赖数组里写什么由我决定,只要我写的值没变,就算函数内部用了别的变量也没关系。
正解依赖数组必须完整列出副作用或计算函数读取的所有响应式值。漏写依赖会导致闭包陷阱——函数内部用的是旧值;多写不会导致正确性问题,但会造成不必要的重复执行。专业团队用 eslint 的 exhaustive-deps 规则强制保证依赖完整。
为何常见:“依赖数组控制执行时机”的心智模型没错,但忽略了“必须包含函数用到的所有变量”这一约束,以为这是可选项。
面试题
Q1. useState 返回的数组中,第二个元素的作用是?
- A.返回组件最新一批 props
- B.触发组件重新渲染函数← 正确答案
- C.监听组件销毁的钩子
- D.缓存计算结果的存储
答案:B。useState 返回 [value, setter],第二个元素是更新函数,调用后把更新动作排队并触发下一次渲染。它不是 props、不是生命周期钩子,更不是缓存区。就像储物柜的钥匙——用它才能开门换新食材。
Q2. 为什么不能在 if 语句中调用 Hook?
- A.if 语句会拖慢组件渲染速度
- B.会导致组件永久无法卸载
- C.依赖调用顺序匹配会错位← 正确答案
- D.Hook 仅限类组件中使用
答案:C。React 用链表按调用顺序存储 Hook,if 语句会让某次渲染少调用一个 Hook,后面的节点全部错位,数据“张冠李戴”。就像厨师把漏勺挂错位置,伸手一抓可能抓到夹子。
Q3. useEffect(callback, []) 的副作用何时执行?
- A.每次渲染完成后都会执行
- B.仅在组件挂载后执行一次← 正确答案
- C.仅在组件卸载前执行一次
- D.永远不会被 React 执行
答案:B。空依赖数组表示“没有任何变量会影响这个副作用”,React 只在首次挂载后执行一次。它等价于 class 组件里的 componentDidMount。注意它依然在绘制完成后执行,不会阻塞首屏。
Q4. 在 setInterval 回调中想读取最新 state,正确的是?
- A.直接在回调中读取 state 值
- B.用 useRef 保存最新值读取← 正确答案
- C.把 state 提升为全局变量
- D.用 useCallback 避免组件更新
答案:B。setInterval 回调是首次渲染的闭包,直接读 state 永远是旧值;useRef 返回的可变对象不会随渲染重建,把最新值同步进 ref.current 就能让回调每次读到新值。全局变量违背 React 数据流,useCallback 也解决不了闭包定格问题。
Q5. 关于 useMemo 的说法,正确的是哪一项?
- A.使用它能跳过所有组件渲染
- B.缓存依赖未变时的计算结果← 正确答案
- C.useMemo 只能用在类组件
- D.useMemo 会直接修改 DOM
答案:B。useMemo 的核心是“依赖不变就不重算”,把昂贵计算的结果缓存起来。它不能跳过渲染,不修改 DOM,函数组件和自定义 Hook 中才能用。就像保鲜盒里的半成品:原料没换,直接拿出来上菜。
Q6. React 如何比较依赖数组是否发生变化?
- A.使用深比较递归检查内容
- B.用 Object.is 逐项进行比较← 正确答案
- C.序列化字符串后比对结果
- D.只比较两个数组的长度
答案:B。React 用的是 Object.is 逐项比较,而不是深比较或字符串序列化。Object.is 类似 ===,但对 NaN 和 +0/-0 的处理更符合直觉。只要有一项不同,就认为依赖变了,对应副作用或缓存需要重新执行。
Q7. Hook 数据在 React 内部存储在哪里?
- A.保存在模块的全局变量中
- B.每次渲染都会重建链表
- C.fiber.memoizedState 链表上← 正确答案
- D.保存在浏览器 localStorage
答案:C。每个组件的 Hook 以单向链表挂在对应 Fiber 节点的 memoizedState 字段上。链表首次挂载后反复复用,不会每次重建,也不存在全局变量里。这正是“调用顺序必须稳定”的根本原因——React 靠位置找数据。
Q8. 下列哪些做法符合 Hooks 调用规则?
- A.在函数组件顶层调用 Hook← 正确答案
- B.在 for 循环内部调用 Hook
- C.在自定义 Hook 中调用← 正确答案
- D.在条件表达式内部调用
答案:AC。Hooks 只能在函数组件顶层或自定义 Hook 中调用。for 循环内部的调用次数可能变化,条件表达式可能导致某次渲染少调一个 Hook,都会破坏链表的顺序匹配,属于违规用法。
Q9. 关于闭包陷阱,下列哪些说法是正确的?
- A.每次渲染都会创建新的闭包← 正确答案
- B.闭包捕获的是渲染时的值← 正确答案
- C.闭包内引用的值时时更新
- D.依赖数组能促使闭包重建← 正确答案
答案:ABD。每次渲染函数体重新执行,新闭包随之产生;闭包冻结的是诞生那一刻的值,不会实时更新。把变量写进依赖数组,会让副作用函数在依赖变化时重建,从而捕获新值。C 恰好说反了,“时时更新”的是 useRef,不是闭包。
Q10. 关于 React 渲染流程中 Hooks 的行为,正确的是?
- A.更新队列中的更新按序执行← 正确答案
- B.渲染阶段会运行副作用函数
- C.多个 setState 会合并重渲染← 正确答案
- D.useMemo 依赖变化必重渲染
答案:AC。useState 的更新队列按顺序依次执行,如同流水线依次加工;React 批处理会把同一批次多个 setState 合并成一次渲染。副作用函数在提交阶段之后执行而非渲染阶段,因为渲染阶段可能被打断。useMemo 依赖变化只会触发重新计算,不会直接触发组件重渲染,D 错误。