想象你要开一家属于自己的餐厅。厨房里需要储物柜(存放食材)、需要自动化的设备(比如洗碗机)、需要提前备菜(缩短出餐时间)、还需要固定的工具摆放位(不用每次找)。React 里的 Hooks 就像厨房里的这些“功能模块”——useState 是储物柜,useEffect 是自动化设备,useMemo 是备菜区,useCallback 是固定工具位。没有它们,你的 React “厨房” 就会杂乱无章、反复炒同一道菜、甚至把厨房点着。
组件每次渲染就像厨师重新进一次厨房——如果每次都要重新切菜、重新找工具、重新洗碗,那效率极低。Hooks 就是帮你把厨房管理得井井有条的“厨房管理系统”。本篇文章用最生活化的方式,把这四大 Hooks 逐个讲透,让你真正理解它们的工作原理、适用场景和常见坑,而不是死记 API。
对 AI 从业者而言,React Hooks 是构建 AI 应用前端界面(如聊天机器人、模型可视化平台)的核心工具。掌握它,你就能自己搭建高效、流畅的 AI 产品界面,而不是只会调 API。
可视化演示
展示useState状态更新与重渲染、useEffect依赖追踪与生命周期、useMemo/useCallback缓存机制的交互演示。
本节配有交互动效(进入视口后自动加载,约 3KB)。
useState状态更新机制 setState触发组件重渲染,新状态替代旧状态 批量更新合并多个setState为一次渲染 setN(v=>v+1) 函数式更新避免闭包陷阱
Hook心智模型
走进一间井然有序的厨房:储物柜里食材分门别类,料理台上刀具悬挂整齐,墙上的计时器滴答作响,冰箱里备着洗好切好的半成品。每一间餐厅的后厨都是这样——每个工位都有明确的功能分区。React 的组件就像这间厨房,而 Hooks 就是厨房里的功能区。理解了这个心智模型,你就掌握了 React Hooks 的底层思路。
React 16.8 之前,函数组件是“无状态的展示组件”,就像一间只有一张桌子的临时摊位,没法存食材、没法用设备。Hooks 的出现相当于给这个摊位升级成了完整厨房——函数组件也能拥有状态和副作用了。其中四个最常用的 useState、useEffect、useMemo、useCallback,分别对应厨房里的储物柜、自动化设备、备菜区、固定工具位。
组件每次渲染,就像厨师重新走进厨房开始做菜。渲染过程中,React 会按顺序执行每一个 Hook——这就是为什么 Hooks 不能放在条件语句或循环里。Hooks 的调用顺序必须每次渲染都完全一致,就像厨房里储物柜的位置不能每天换一样。React 靠调用顺序来匹配每次渲染对应的状态,如果顺序变了,状态就会错乱。
回到厨房的比喻:为什么每次渲染都要重新“进厨房”?因为 React 需要检查食材是否变了、客人有没有新要求。Hooks 的价值在于让厨房每次都能保持同样的布局——厨师闭着眼都能拿到想要的东西。这就是 Hooks 心智模型的核心:稳定的厨房布局 + 按序取用。
- 渲染函数渲染结果是输入状态和属性的函数,像“菜品 = 食材 + 菜谱 + 厨具”。
- 状态参数组件内部通过 useState 管理的数据,像储物柜里的食材。
- 属性参数父组件传入的数据,像客人下单时指定的口味偏好。
- 上下文全局共享的数据,像厨房里公用的调料架。
useState状态管理
厨房的储物柜是最基础也最重要的设施。不管什么食材,先得有地方放。useState 就是你厨房里的储物柜——它让组件记住自己的状态,并且在状态变化时触发重新渲染(相当于食材补齐后通知厨师开工)。
useState 的基本用法是传入初始值,返回一个数组:当前状态和一个更新函数。这个更新函数就像储物柜的钥匙——只有通过它才能修改柜子里的东西。但这里有个细节:如果你直接写 setCount(count + 1),React 可能不会按你想象的方式执行,因为 count 可能是个“过期”的值。这就是为什么 React 提供了函数式更新:
另一个重要机制是批量更新。React 18 自动批处理状态更新:在一个事件处理函数里连续多次 setState,React 会把它们合并为一次渲染——就像厨师在同一时间把多种食材一起放进储物柜,而不是每放一种就停下来检查一遍。这在大多数时候是好事(提升了性能),但也意味着你不能依赖在同一个函数里“立即读取”更新后的状态。
State 与渲染的关系:每次状态变化都会触发一次新的渲染(重新进厨房做菜)。但有些人会误把状态当普通变量,直接在组件里改它的值——这就像你把食材从储物柜拿出来放在台面上,然后又放回去,但没告诉管理员食材变了,厨房自然不知道需要重新做菜。永远不要直接修改 state 变量的值,必须通过 setState 函数来更新。
回到厨房比喻:useState 的储物柜设计体现了一个关键原则——单一数据源。所有食材都从同一个柜子里取,更新也必须通过管理员。这避免了两处记录同一件事但不一样(数据不一致)的问题。理解了这个,你就理解了 React 数据流的核心。
- 更新函数setState 是更新状态的唯一入口,相当于储物柜管理员。
- 回调参数传入一个函数,接收上一次的状态值作为参数。
- 计算结果React 确保 prev 一定是最新值,你只需返回新状态。
useEffect副作用
厨房里有些设备不需要厨师主动操作:洗碗机洗完碗自动停止、烤箱烤好自动响铃、排风扇到点自动打开。useEffect 就是 React 厨房里的“自动化设备”——它让组件渲染完成后自动执行一些操作,比如发请求、操作 DOM、设置定时器。
但自动化设备有个特点:配置不当会反复触发或泄漏。useEffect 的第二个参数——依赖数组——就是设备的“触发条件”。依赖数组里放什么,决定副作用什么时候重新执行:
依赖数组有一个关键行为:如果不传第二个参数,副作用在每次渲染后都执行——像排风扇每次进厨房都自动打开。传了空数组 [],则只在组件挂载时执行一次。传了具体依赖,则在依赖变化时执行。依赖数组应该包含所有你在副作用中用到的响应式值——如果漏掉了,就会出现“闭包陷阱”(后面会详细讲)。
cleanup 清理函数:useEffect 的另一个核心概念是清理函数。比如你设置了定时器,组件卸载时应该清除它,否则定时器还在运行——就像洗碗机洗完碗后还在空转,浪费水电。在 useEffect 中返回一个函数,React 会在组件卸载时或下一次执行副作用前调用它:
执行时机:useEffect 里的代码不是在浏览器绘制前执行的,而是在浏览器绘制完成之后,类似放在了 componentDidMount 或 componentDidUpdate 的位置。这意味着如果副作用里需要读取 DOM 或发起请求,这些操作不会阻塞页面渲染——但如果你需要更早的执行时机,可以使用 useLayoutEffect(不过大多数场景不需要)。
回到厨房比喻:useEffect 最需要警惕的是“副作用外溢”。洗碗机(定时器)如果没关好,水会漏到整个厨房(内存泄漏)。每次你启动一个自动化设备,都需要想清楚:它什么时候该停?什么时候该重新配置?这就是依赖数组和 cleanup 存在的意义。
[] 表示只在进厨房(组件挂载)时运行一次;不传数组则每次都运行。- 副作用函数第一个参数 setup 函数,包含要在渲染后执行的逻辑。
- 依赖数组第二个参数 controls,列出影响副作用执行的状态变量。
- 依赖比较React 用 Object.is 对每个依赖做对比,只有变化时才重新执行副作用。
useMemo缓存计算
餐厅生意红火时,厨师每天都要切几十斤土豆丝。聪明的厨师会每天开工前一次性切好大量半成品放在冰箱里,客人点单时直接取用——这就是 useMemo 的思维。它用于缓存计算量较大的操作的返回值,避免每次渲染都重复计算。
useMemo 接受一个纯计算函数和依赖数组,只有依赖变化时才重新执行计算函数,否则直接返回上一次缓存的结果:
刚才提到 useMemo 缓存的是计算结果。但在函数组件中,每次渲染时其中的所有代码都会重新执行——包括函数声明、对象字面量、数组创建。当这些“对象”作为 Props 传给子组件时,即使内容没变,引用也变了(就像你每次都拿个新袋子装同样的菜)。useMemo 不仅缓存计算结果,还能让对象引用在依赖不变时保持稳定。
适用场景:主要适用于计算量大的场景,比如复杂列表过滤、数据转换、大量数据的统计计算。对于简单计算(如 1 + 2),用 useMemo 反而是浪费——就像为了切一根葱也专门开一次备菜流程,反而增加了复杂度。
不能用来干什么:useMemo 不要用于副作用操作(如发请求、修改 DOM),因为它的计算函数应该是纯的。它也不能替代 ref——如果你要在渲染之间共享可变引用,应该用 useRef,就像备菜区的东西不能放冰箱以外的区域,每种设施各司其职。
回到厨房比喻:备菜区(useMemo)的存在是为了减少重复劳动,但备菜本身也需要时间。如果一道菜 10 秒就能做完,专门备菜(比如切好放冰箱)反而多花 5 秒——这就是为什么 React 官方强调不要过度使用 useMemo,先测量性能瓶颈再优化。
- 计算函数第一个参数是纯函数,只根据输入计算输出,不修改外部状态。
- 依赖参数第二个参数列出影响计算结果的状态变量。
- 缓存比较React 存储上一次的 result 和 deps,渲染时对比 deps 是否变化。
useCallback缓存函数
厨房里的刀具如果每天都换位置,厨师每次做饭都要找半天。优秀的厨房一定会给每把刀固定一个位置。useCallback 就是 React 厨房里的“固定工具位”——它缓存一个函数实例,在依赖不变的情况下返回同一个引用。
为什么函数引用稳定如此重要?因为在 React 中,每次渲染都会重新创建函数。如果你把函数作为 Prop 传给经过 React.memo 优化的子组件,父组件每次渲染时新的函数引用会导致子组件不必要的重新渲染——就像你每次都递给孩子一把新刀,孩子每次都得重新适应手感。
useCallback 和 useMemo 的区别:很多人会混淆这两个 Hook。useCallback 缓存的是函数本身(固定工具位),useMemo 缓存的是计算的结果值(备菜区)。useCallback(fn, deps) 实际上等价于 useMemo(() => fn, deps)——两者底层机制相同,只是语义不同:一个是“我要复用这个函数”,一个是“我要复用这个计算结果”。
依赖数组的一致性:useCallback 的依赖数组必须包含函数体内所有使用到的响应式值。如果函数内部访问了 state 或 props 但没写进依赖数组,这个函数就会一直引用旧值——这就是“闭包陷阱”的经典形态:厨师拿到的菜单是昨天的,菜谱上写的是昨天的食材清单。
回到厨房比喻:useCallback 的固定工具位思维提醒我们——函数也是对象。每次渲染创建新函数,就像每天换一把新刀,不仅浪费,还会让依赖这个工具的人(子组件)无所适从。保持引用的稳定,才能让整个厨房(组件树)高效协作。
- 函数声明第一个参数是要缓存的函数,每次渲染传入新函数,但只有依赖变化时返回的才是新函数。
- 依赖比较React 对比依赖数组。依赖没变,直接返回上次缓存的函数。
- 配合 memo子组件用 React.memo 包裹,父组件函数引用不变,子组件跳过重新渲染。
闭包陷阱
假设厨师周一看了菜单,记住了“明天要做宫保鸡丁”的配方。周三真正做菜时,他用的还是周一的记忆——但食材早就换了。这就是闭包陷阱(Stale Closure):函数记住了创建它的那个时刻的值,即使外面的状态已经变了。
React 中的闭包陷阱最常见的出现场景是 useEffect 或 useCallback。比如你在 useEffect 中使用了 count 但依赖数组只写了 [],那么这个 useEffect 里的函数体捕获的是组件首次渲染时的 count = 0。即使 count 之后变成 5,这个 effect 里读到的仍然是 0——就像厨师一直拿着一本过期的菜单在做菜。
怎么解决闭包陷阱?最常用的方法是函数式更新:
函数式更新让 React 替你管理“当前值”,你不直接使用可能过期的 count 变量,而是告诉 React“基于最新值计算”。这就像你告诉储物柜管理员“把食材数量加一”,而不是自己先看一眼当前数量(可能已经过期)再加一。
另一种解决方法是使用 useRef 保存最新值:ref.current 永远是最近一次渲染的值,可以绕过闭包陷阱。这在事件监听器(比如 addEventListener)中特别有用——监听器只创建一次,但其内部需要访问最新的状态。
依赖数组遗漏也是闭包陷阱的高发区。React 官方有一个 ESLint 插件 eslint-plugin-react-hooks 专门检查依赖数组是否完整——一定要使用它。把依赖写全,React 就能确保每次 effect 运行时的闭包都是最新的。回到厨房比喻:闭包陷阱就是“记忆中的菜单”。食材(state)已经更新换代,但厨师手里的菜单还是三天前的。要么换成最新菜单(加依赖),要么让管理员为他提供实时食材清单(函数式更新)。
- 闭包定义函数内部访问了外部作用域的变量,这个函数就是闭包。
- 过期捕获当函数创建后外部变量更新,函数内的引用仍然指向旧值。
- React 场景每次渲染都会创建新的函数,新函数才会捕获新的 state 值。
// 错误:捕获了旧值setCount(count + 1); // 正确:总是基于最新值更新setCount(prev => prev + 1);
性能优化实践
当厨房运转了一段时间后,你会发现有些流程可以优化:哪些材料可以提前备好?哪些厨具使用频率低可以收起来?React 应用的性能优化也是这个思路。前面讲的 useMemo 和 useCallback 是主要的优化工具,但它们不是万能的。
第一条原则:缓存不是免费午餐。useMemo 和 useCallback 本身也有开销——React 需要存储依赖数组、进行对比。对于计算量小的场景,使用缓存反而更慢。React 官方文档建议:先测量再优化,找到真正的性能瓶颈再动手。
第二条原则:依赖数组越精简越好。观察以下代码:
1. 最小化依赖:依赖数组里变量越多,缓存的失效频率越高。
2. 拆分状态:将大型状态拆成多个小状态,减少依赖的粒度。
3. 组件拆分:将复杂的组件拆分为多个子组件,配合 memo 隔离重渲染范围。
第三条原则:状态提升与下放。如果一个状态只被某个子组件使用,它就应该放在那个子组件内部——就像盐和糖的调料瓶应该放在灶台旁边,而不是放在餐厅门口的储物间。反过来,多个子组件共享的状态才需要提升到父组件(放在公共区域)。这是 React 社区称为“状态就近原则”的核心思想。
第四条原则:使用 Profiler 测量。React 提供了 <Profiler> 组件和 React DevTools 的 Profiler 标签页,可以记录组件的渲染耗时。找出渲染耗时最长的组件,再决定是否使用 useMemo/useCallback/React.memo。盲目添加缓存就像在厨房里为每个动作都装一台自动化设备——大部分设备都闲置,还占了宝贵的空间。
回到厨房比喻:性能优化的最终目标是减少不必要的工作。React 默认每次渲染会重跑所有代码(每个厨师都从零开始做菜),优化手段就是让 React 知道“这些食材没变、这道菜不用重做”。每个 Hook、每个缓存都有成本,只有当你清楚知道“工作量”在哪里时,优化才有效。
$$\text{useMemo}(() \Rightarrow \text{expensive}(\text{a}, \text{b}), [\text{a}, \text{b}])$$AI 应用场景
构建高效 AI 聊天前端
阶段:前端界面开发
解决的问题:AI 聊天应用需要实时显示响应流、处理用户输入、维护对话历史。传统方式每次输入都触发整个组件树重新渲染,导致页面卡顿。用 useMemo 缓存对话列表的格式化结果、useCallback 稳定发送消息的函数引用,配上 React.memo 隔离消息项的渲染,能显著降低交互延迟。
具体案例:一个 LLM 聊天页面,对话消息有几百条,每条包含文本、代码块、Markdown 渲染。如果每条消息都做 Markdown 解析(重度计算),每次输入新消息都会卡顿。用 useMemo 对每条消息的解析结果做缓存,依赖是消息文本本身——文本不变就直接复用解析结果,让打字输入保持流畅。
流式响应中的闭包陷阱防护
阶段:AI 模型输出解析与状态管理
解决的问题:流式响应中,WebSocket 或 SSE 回调里如果使用了过期的 state,会导致状态更新丢失——例如读取了旧的对话 ID。用函数式更新和 useRef 保存最新值的方案,确保回调总是操作最新的对话状态。
具体案例:在流式输出过程中,用户在界面上切换了对话。如果回调闭包捕获了旧对话 ID,AI 的回复会被追加到错误的历史记录中。通过 useRef 保存最新对话 ID,回调里总是读取 ref.current,问题迎刃而解。
模型推理参数面板优化
阶段:推理参数配置 UI
解决的问题:大模型配置面板(temperature、top_p、max_tokens 等参数)通常需要多个滑块实时联动。每次拖动滑块都触发频繁重渲染,导致交互不跟手。用 useCallback 稳定滑块变化处理函数,结合拆分独立的参数子组件,让每个滑块的更新只重渲染自身。
具体案例:一个包含温度、top_p、top_k、max_tokens 四个参数的推理面板,四个滑块独立组件化 + useCallback 稳定回调 + React.memo 拦截无关传参,拖动一个滑块时其他滑块不再闪烁重排。
Python 演示
# 模拟 React Hooks 核心机制的最小 Python 实现class Component: def __init__(self, name): self.name = name self._states = {} # 状态槽位 self._effects = [] # 副作用列表 self._memos = {} # 缓存槽位 self._call_index = 0 # Hook 调用顺序索引 self._count = 0 # 状态计数器 def useState(self, initial): idx = self._count # 当前 Hook 序号 self._count += 1 # 槽位自增计数 slot = "state_" + str(idx) if slot not in self._states: self._states[slot] = initial value = self._states[slot] def setState(new_val): # 支持函数式更新 if callable(new_val): self._states[slot] = new_val(self._states[slot]) else: self._states[slot] = new_val return value, setState def useEffect(self, fn, deps): slot = "effect_" + str(len(self._effects)) entry = { "fn": fn, "deps": deps, "cleanup": None, "prev_deps": None } self._effects.append({"slot": slot, "entry": entry}) def useMemo(self, compute, deps): slot = "memo_" + str(len(self._memos)) key = str(deps) # 用依赖列表的字符串表示做缓存键 if key in self._memos: return self._memos[key] result = compute() self._memos[key] = result return result def render(self): # 每次渲染重置调用索引 self._count = 0 view = "Render of " + self.name # 模拟组件挂载后执行副作用 for e in self._effects: entry = e["entry"] if entry["prev_deps"] != entry["deps"]: # 先清理上一次的副作用 if entry["cleanup"]: entry["cleanup"]() entry["cleanup"] = entry["fn"]() entry["prev_deps"] = entry["deps"] return view def demo(): comp = Component("Counter") count, setCount = comp.useState(0) def tick(): print("Timer set, count now:", comp._states["state_0"]) comp.useEffect(tick, []) # 第一次渲染 comp.render() # 模拟点击 +3 setCount(lambda prev: prev + 1) setCount(lambda prev: prev + 3) print("After updates, value:", comp._states["state_0"]) # 第二次渲染 comp.render() if __name__ == "__main__": demo()
if slot not in self._states首次渲染才初始化状态,模拟 useState 的惰性初始化setState` 内部判断可调用对象 — 对应 React 的函数式更新 `setState(prev => prev + 1)self._count递增模拟 Hook 调用顺序 — 这就是为什么不能条件调用 Hook"effect_" + str(len(self._effects))用列表长度做槽位,避免属性与方法同名问题entry["cleanup"] = entry["fn"]()保存清理函数,下次执行副作用前先调用key = str(deps)简化版缓存键,使用依赖数组的字符串表示
常见误区
误区“useState 传一个函数作为初始值,这个函数会在每次渲染时都执行一遍,所以要用 useMemo 去包一下。”
正解useState(() => expensive()) 中的初始化函数只在组件首次挂载时执行一次,React 内部会记住这个初始值,后续渲染直接忽略。这就像储物柜只在开业时放一次食材,之后都是直接开柜取用——不是每次做菜都重新进货。
为何常见:因为 JSX 中的代码每次渲染都会执行,初学者看到 useState(表达式) 会以为函数也被每次调用,其实 React 对初始值做了特殊处理。
误区“依赖数组应该把所有变量都写进去,不管用不用得到,这样最安全。”
正解依赖数组写太多无关变量,会导致 effect 频繁执行。正确的标准是:只包含 effect 内部实际使用到的响应式值。用 ESLint 插件自动检查即可,不用手工全写。就像洗碗机的感应器只应该检测“是否有脏碗”,如果连“客人走了没”“灯关了没”都监控,就会乱启动。
为何常见:因为闭包陷阱的教训让大家记住了“依赖要写好”,但走到了另一个极端。
误区“useMemo 和 useCallback 是性能优化神器,只要用了它们应用就会变快,所以所有函数和计算都要包一层。”
正解缓存本身有开销——存储依赖、比较依赖、垃圾回收压力。对于简单计算,加了 useMemo 反而更慢。React 官方的建议是:先测量,发现问题再用优化。就像给每个动作都配一台自动化设备,大部分时候设备在待机,还占厨房空间。
为何常见:性能优化的话题容易让人产生“多多益善”的直觉,但缓存的成本在多次渲染后才会被收益覆盖,需要权衡。
误区“闭包陷阱只存在于 useCallback 中,只要不用 useCallback 就不会有闭包问题。”
正解闭包陷阱可能出现在任何创建函数的场景——useEffect、事件监听器、setTimeout 回调等。只要函数捕获的变量在当前渲染之外被读取,就可能读到旧值。本质是 JavaScript 闭包特性与 React 渲染模型的互动问题,而非某个 Hook 特有。
为何常见:因为 useCallback 的依赖数组写法容易踩坑,很多人就以为问题出在 useCallback 本身。
误区“state 就是一个变量,我直接 count = 5 然后调用 setState,或者直接改 state 对象的属性,就能更新界面。”
正解直接修改对象属性的方式(如 obj.a = 1)不会触发重新渲染,因为 React 通过 Object.is 比较新旧状态引用,修改同一个对象引用时不会检测到变化。正确做法是创建一个新对象(展开或结构拷贝),然后 setState 传入新对象。就像你不能只把储物柜里的食材换掉而不通知管理员——管理员(React)只认“换了一整个柜子”这个信号。
为何常见:从普通 JavaScript 转向 React 时,容易保留“直接赋值”的直觉,没意识到状态更新需要走触达机制。
面试题
Q1. 在 React 中,函数组件使用哪个 Hook 来管理本地状态?
- A.useState← 正确答案
- B.useEffect
- C.useMemo
- D.useRef
答案:A。useState 是用于声明组件内部状态的 Hook,返回当前状态和更新函数。useEffect 处理副作用,useMemo 缓存计算结果,useRef 管理可变引用。就像储物柜(useState)是存放食材的地方,自动化设备(useEffect)是另一回事。
Q2. 关于 useState 的函数式更新,下列说法正确的是?
- A.比普通更新更节省内存
- B.可以避免闭包中捕获过期状态← 正确答案
- C.只能在 useEffect 中使用
- D.函数参数代表初始值
答案:B。函数式更新 setState(prev => prev + 1) 不直接依赖外部变量,React 会传入最新状态值,因此避免闭包陷阱。它没有内存优势,也不限使用场景。选项 D 描述的是 useState 初始化时的惰性初始化函数,二者作用完全不同。
Q3. useEffect 第二个参数是空数组 [] 时,副作用函数会?
- A.每次渲染后都执行
- B.在组件卸载时执行
- C.只在组件挂载时执行一次← 正确答案
- D.永远不执行
答案:C。空数组表示没有任何依赖项,React 只在组件首次挂载后执行一次副作用。如果完全省略第二个参数,则每次渲染后都会执行。cleanup 函数仍然会在组件卸载时执行。
Q4. 以下哪些做法能有效避免闭包陷阱?
- A.在 useEffect 中使用函数式更新← 正确答案
- B.把所有用到的 state 都放进依赖数组← 正确答案
- C.使用 useRef 保存最新值← 正确答案
- D.使用 useState 直接读取状态
答案:ABC。函数式更新让 React 管理最新值,完整依赖数组确保 effect 内部闭包是最新的,useRef 提供了一个跨渲染的稳定可变对象。选项 D 中直接读取状态变量、不通过 setState 回调,在异步回调中往往读到的是过期引用。
Q5. useMemo 和 useCallback 的核心区别是什么?
- A.useMemo 缓存计算结果,useCallback 缓存函数本身← 正确答案
- B.useMemo 用于异步操作,useCallback 用于同步操作
- C.useMemo 用于状态,useCallback 用于 Props
- D.它们没有任何区别
答案:A。useMemo(() => value, deps) 缓存的是计算的结果值,useCallback(fn, deps) 缓存的是函数引用。二者底层实现相通(useCallback 可看作 useMemo 的语法糖),但语义不同。就像备菜区(useMemo)存的是切好的菜,固定工具位(useCallback)放的是刀本身。
Q6. 下列代码存在什么问题?
- A.定时器不会运行
- B.count 永远停留在初始值,且定时器没有清理← 正确答案
- C.每次渲染都会重新创建定时器
- D.setCount 不是合法的更新函数
答案:B。空依赖数组导致 effect 只执行一次,但内部 setInterval 回调捕获的是首次渲染的 count=0,之后每次执行都是 setCount(0+1),状态不会持续增长。同时没有返回 cleanup 函数,组件卸载后定时器仍然存在,造成内存泄漏。应改为函数式更新 setCount(prev => prev + 1) 并实现 cleanup 清理定时器。
Q7. 关于 React.memo 与 useCallback 的配合,正确的说法有?
- A.React.memo 只对函数组件生效,对普通函数无效← 正确答案
- B.父组件传给子组件的函数经过 useCallback 缓存后,配合 React.memo 才能避免子组件重渲染← 正确答案
- C.React.memo 会阻止所有子组件重渲染
- D.不传函数给子组件时,React.memo 仍然有效← 正确答案
答案:ABD。React.memo 是浅比较 Props 的高阶组件,只对函数组件生效。每次渲染时父组件创建的新函数引用会让 memo 的比较失败(因为引用不同),useCallback 能让函数引用稳定,memo 才有意义。但 React.memo 不代表绝对不渲染,它只拦截 Props 变化导致的渲染,组件内部状态变化仍会渲染。
Q8. 当一个组件卸载时,useEffect 返回的清理函数会被调用,它主要用于?
- A.清除组件内部的所有状态
- B.清理定时器、取消订阅、移除事件监听等← 正确答案
- C.强制组件重新渲染
- D.保存组件当前状态
答案:B。清理函数用于释放副作用资源——清除定时器、取消网络请求、移除事件监听器、断开 WebSocket 等,防止内存泄漏。就像洗碗机洗完碗后自动关水关电。它不会清除状态、不会触发渲染。
Q9. 以下哪个 Hook 最适合缓存一个复杂的计算结果?
- A.useRef
- B.useCallback
- C.useMemo← 正确答案
- D.useEffect
答案:C。useMemo 专门用于缓存计算结果,在依赖不变时返回旧值,避免重复执行昂贵计算。useCallback 缓存函数引用,useRef 管理可变引用,useEffect 处理副作用。
Q10. React 18 中关于 useState 批量更新的说法,正确的是?
- A.同一个事件处理函数中的多次 setState 会被合并为一次渲染← 正确答案
- B.在 Promise 回调中的 setState 也会被批量处理← 正确答案
- C.setState 是同步更新状态的
- D.批量更新让状态“立即生效”
答案:AB。React 18 的自动批处理涵盖 Promise、setTimeout 和原生事件处理器。批量更新意味着点击后多个 setState 合并成一次渲染,因此状态不会立即更新到 UI。选项 C、D 都是对更新时机的误解——读取此时的状态仍然可能是旧值(除非用函数式更新确保基于最新值)。