想象你是一家咖啡馆的老板。每天早晨,咖啡师按照标准配方制作拿铁:研磨豆子、萃取浓缩、打奶泡、融合拉花。这套流程可以写成一张“配方卡”,任何新员工照着做都能出品一致的咖啡。 React 的函数组件就是这样一张配方卡——它是一个纯粹的“函数”:接收原料(props),结合内部状态(state),产出咖啡(UI)。但在 React 早期,事情远没有这么清爽。那时候管理状态要用类组件,就像要求每位咖啡师必须先在总部挂名登记,填写复杂的“员工信息表”(this、constructor、各种生命周期方法)——写起来冗长,理解起来绕弯。 Hooks 的出现,就像是给咖啡师发了一套便携工具包:一个小型豆仓(useState)、一张保养清单(useEffect)、一本配方速查卡(useCallback)、一个共享物料柜(useContext)……它们让“制作咖啡的流程”变得更专注、更模块化。 本文将带你从零理解 React Hooks 与函数组件设计。我们会用“咖啡馆经营”这个比喻贯穿始终,逐步拆解每个 Hook 的用途、原理与最佳实践。读完你会明白:函数组件不是“没有状态的组件”,而是“把状态管理工具带在身上的轻量工作站”。

岗位关联

掌握 React Hooks 是现代前端工程化的必修课。无论你是 AI 产品的前端开发者,还是需要理解大模型应用中链式交互架构的工程师,Hooks 的“依赖追踪与副作用管理”思路都与你的日常工作同构。

目录
  1. 可视化演示
  2. 函数组件崛起
  3. useState 状态管理
  4. useEffect 副作用管理
  5. useMemo 性能优化
  6. useCallback 函数缓存
  7. useContext 跨层通信
  8. useRef 引用操作
  9. 自定义Hook封装
  10. AI 应用场景
  11. Python 演示
  12. 常见误区
  13. 面试题

可视化演示

演示Hooks生命周期执行流程、状态管理与依赖追踪、useMemo性能优化防止不必要的重渲染。

本节配有交互动效(进入视口后自动加载,约 3KB)。

Hooks生命周期 挂载→更新→卸载执行流 useEffect依赖追踪与清理 effect=()=>{...};cleanup=effect() 函数组件通过Hooks管理副作用与生命周期

函数组件崛起

想象你接手了一家老牌咖啡馆。前任老板留下了一套厚重的“员工登记手册”:姓名、工号、排班记录、调岗历史……这套体系来自二十年前的连锁总部,功能齐全,但新员工光是理解手册就要培训两周。这就是类组件时代的写照。

React 早期的类组件,写一个“计数器”需要定义 class、绑定 this、背诵生命周期方法。逻辑被拆散在 constructor、componentDidMount、componentDidUpdate 三个“账本”里,就像把咖啡豆、牛奶、糖的库存分别记在三本不同的账册上,查一次库存要翻半天。

函数组件彻底改变了这个局面。它就是一个普通函数:接收 props,调用 Hooks 声明状态,最后返回 JSX。React 调度器负责在合适的时机执行这个函数。函数组件没有 this、没有实例,只有清晰的“输入 → 输出”映射。

回到咖啡馆:函数组件就是吧台上那张配方卡——把“豆量、水温、萃取时间”作为输入,固定产出“一杯拿铁”。配方卡不需要工号登记,不需要等待总部审批,每个员工看一眼就会做。Hooks 带来的观念升级就在于此:组件不再是“管理对象的档案袋”,而是“根据输入产出输出的函数”。

UI=f(state,props)\displaystyle UI = f(\text{state}, \text{props})
这个公式是函数组件的“心法”:UI 是输出,state 和 props 是输入,组件本体就是那个“加工函数”。
  1. 识别 state 输入state 是组件自身的内部状态,比如计数器当前的数值,它通过 Hook 在组件内声明。
    state\text{state}
  2. 识别 props 输入props 是父组件传入的外部参数,等价于“顾客递过来的点单小票”。
    props\text{props}
  3. 理解函数 ff 是函数组件的渲染逻辑,它决定“输入如何变成输出”。每次渲染就是一次新的函数调用。
    f(⋅)f(\cdot)
  4. 理解输出 UIUI 是 React 最终渲染到页面上的元素树,是函数的“作品”。
    UI\text{UI}

useState 状态管理

饮品台上那个透明玻璃豆仓,让咖啡师一眼就知道还剩多少豆。但在老系统里,豆量数据锁在总部保险柜中,每次查看都要填申请表——这就是类组件里 this.state 的使用体验。

useState 就是 React 发给每个函数组件的“迷你豆仓”。调用一次,就在组件上挂载一个状态变量,返回值是“当前值 + 更新函数”的组合。

useState 的更新函数并不是“立即改值”,而是通知 React“这个组件需要重新渲染”。React 会把同一事件里的多次 setState 合并成一次渲染(批量更新)。同时,state 必须遵守不可变原则:要创建新值而非修改原值,否则 React 难以察觉变化。

豆仓的好处在于:你只关心“往豆仓里加了多少豆”,至于补货单怎么流转、报表怎么生成,那是 React 内部的事。咖啡师拿到了简单好用的工具,库存管理的复杂度被封装了起来。

[state, setState]=useState(initialValue)\displaystyle \left[ \text{state},\ \text{setState} \right] = \text{useState}(\text{initialValue})
左边数组解构拿到“当前值”和“更新器”,右边传入初始值。setState 不直接修改变量,而是提交一次“状态变更申请”。
  1. 调用 useState在组件顶层声明一个状态变量,传入初始值。
    useState(initialValue)\text{useState}(\text{initialValue})
  2. 数组解构`通过解构语法同时取得当前状态和更新函数,二者成对出现。
    [state, setState]\left[ \text{state},\ \text{setState} \right]
  3. 调用 setState更新函数被调用后,组件被标记为“需要重渲染”。
    setState(newValue)⇒re-render\text{setState}(\text{newValue}) \Rightarrow \text{re-render}

useEffect 副作用管理

咖啡店每天打烊后有一套固定清洁流程:拆机、清渣、除垢、复位。这套流程不属于“做咖啡”本身,但它必须在每天结束时自动执行。useEffect 就是 React 为你安排的“打烊流程”。

函数组件的主流程是渲染 UI,但真实应用还需要请求数据、订阅事件、操作 DOM。这些“渲染之外的事”就是副作用。useEffect 让你声明“当组件渲染完成后,执行这段副作用”。

useEffect(fn, deps) 中 deps 是依赖数组。React 在每次渲染后对比依赖是否变化:若变化,先执行上一次 effect 返回的清理函数,再执行新的 effect。空数组 [] 表示只在挂载时执行一次;不传数组则每次渲染都执行。

清洁流程被写成 SOP 卡片贴在打烊区:每天下班自动执行,换了新清洗液(依赖变化)就按新卡片操作。这套流程就是咖啡馆的“生命周期”——useEffect 把生命周期方法们统一成了同一张卡片。

useEffect(effect, [deps])⇒effect 于渲染完成后执行\displaystyle \text{useEffect}(\text{effect},\ \left[\text{deps}\right]) \quad \Rightarrow \quad \text{effect}\ \text{于渲染完成后执行}
effect 是“打完烊才做的事”,依赖数组是“哪些条件变化了才需要重新做”。
  1. 声明副作用函数effect 是你要延迟执行的函数,例如拉取数据、订阅事件。
    effect(⋅)\text{effect}(\cdot)
  2. 给出依赖数组声明“哪些值变化时才触发”,空数组表示仅首次生效。
    [deps]\left[\text{deps}\right]
  3. 理解执行时机副作用不在渲染过程中执行,而在 DOM 更新完成后执行。
    render→commit→effect\text{render} \to \text{commit} \to \text{effect}

useMemo 性能优化

人手充足的咖啡馆会在早高峰前预磨一批咖啡粉备着,客人点单时直接萃取,不必临时磨豆。这就是 useMemo 的思路:把昂贵的“磨粉”活儿提前做好,参数不变就直接复用。

函数组件每次渲染都会重新执行内部所有代码。如果某段计算很昂贵(如大数组排序、深拷贝对象),且依赖没有变化,就没有必要每次重算。

useMemo(() => compute(a, b), [a, b]) 会在依赖不变时返回上一次的缓存值,依赖变化才重新执行计算函数。注意:useMemo 是性能优化而非语义保证——React 理论上可能丢弃缓存,所以不能在计算函数里写有副作用的操作。

预磨咖啡粉有保鲜期,依赖就是“这批粉的烘焙日期”。日期没变,就用旧粉;日期变了,必须重新磨。合理安排缓存,能让咖啡师在高峰时段从容出品——当然,隔夜的粉再方便也该倒掉,这正是 useMemo 的边界。

useMemo(f, [deps])={cacheddeps 未变化f(x)deps 已变化\displaystyle \text{useMemo}(f,\ \left[\text{deps}\right]) = \begin{cases} \text{cached} & \text{deps 未变化} \\ f(x) & \text{deps 已变化} \end{cases}
依赖未变走“缓存通道”,依赖变化走“重新计算通道”——像一个双路开关。
  1. 封装计算函数把昂贵的纯计算逻辑放进 f 中。
    f(x)f(x)
  2. 比较依赖数组React 拿新的 deps 与上次的 deps 做逐项对比。
    [deps]\left[\text{deps}\right]
  3. 判断缓存决策依赖未变则直接读取缓存,变化则重新执行 f 并更新缓存。
    cached    vs    f(x)\text{cached} \;\; \text{vs} \;\; f(x)

useCallback 函数缓存

咖啡馆有一张“心形拉花配方卡”,上面画着每一步手法。这张卡是固定的,不需要每次做咖啡都重新画一张。useCallback 就是为了让“函数的引用”保持稳定而存在的。

在 React 中,函数组件每次渲染都会创建一个新的函数对象。如果把一个内联箭头函数作为 props 传给被 React.memo 包裹的子组件,子组件会发现“这个 props 的引用每次都变”,缓存失效、被迫重渲染。

useCallback(fn, deps) 返回的是“依赖不变时保持一致引用”的 fn 版本。它与 useMemo 的关系很微妙:useMemo(() => fn, deps) 几乎等价于 useCallback(fn, deps)——useMemo 缓存返回值,useCallback 缓存函数本身。

配方卡如果每天重画,即使内容相同,员工也要被迫重新学习一份“新文件”——这就是函数引用变化造成的无效劳动。有了固定引用,memo 子组件才能识别“这个回调没变”,放心跳过重渲染。

useCallback(f, [deps])→fstable\displaystyle \text{useCallback}(f,\ \left[\text{deps}\right]) \to f^{\text{stable}}
f 的稳定版本:依赖不变,返回同一个函数引用;依赖变化,才生成新函数。
  1. 传入原函数f 是需要在渲染间保持稳定的回调函数。
    ff
  2. 依赖追踪deps 决定何时需要“换一张新卡”。
    [deps]\left[\text{deps}\right]
  3. 稳定化输出依赖未变时,React 返回上次保存的函数引用。
    fstablef^{\text{stable}}

useContext 跨层通信

连锁咖啡馆的总部会通过广播系统同步“今日特调”和“会员折扣”给所有门店,不需要逐家打电话。useContext 就是 React 世界里的“总部广播”。

当主题、用户信息、语言包等全局数据要传给深层组件时,逐层透传 props 非常痛苦。Context 提供了“在组件树上广播数据”的通道。

React.createContext 创建上下文对象;<Context.Provider value={...}> 在树的某个位置注入值;任意后代组件调用 useContext(Context) 即可读取。当 Provider 的 value 变化时,所有消费该 Context 的组件自动重渲染。

广播台的效率在于“一次播报,全员知晓”。但广播也不是万能的:全员收听带来的重渲染开销,就像所有门店同时被总部消息轰炸。因此,对频繁变化的数据要谨慎控制 Context 的粒度与层级。

value=useContext(MyContext)\displaystyle \text{value} = \text{useContext}(\text{MyContext})
一行代码,从“最近的 Provider”处读取共享值。
  1. 创建上下文createContext 生成一个可跨层传递的上下文对象。
    Context=createContext(default)\text{Context} = \text{createContext}(\text{default})
  2. 注入共享值Provider 组件在树中广播 value。
    Provider(value)\text{Provider}(\text{value})
  3. 读取共享值任意后代组件调用 useContext 获取当前值。
    useContext(Context)→value\text{useContext}(\text{Context}) \to \text{value}

useRef 引用操作

咖啡师围裙口袋里总有一张便签,写着常客的偏好或今天已补的货。这张便签不影响“做咖啡”的主流程,但随时可以翻看。useRef 就是那张便签。

有些值需要跨渲染保留,但每次修改又不值得触发重渲染——比如定时器 id、DOM 节点引用、上一次的某个值。useRef 就是为这类场景设计的。

useRef(initialValue) 返回一个 { current: initialValue } 对象。这个对象在组件整个生命周期内保持引用稳定;修改 current 不会引起重渲染。常见用法是操作 DOM:<input ref={inputRef} /> 然后 inputRef.current.focus()。

便签的内容改了不必汇报总部,也不用重新培训流程——它只属于这位咖啡师的个人工作区。useRef 很适合放置“需要记住但不影响出品”的临时信息,比如定时器、DOM 节点、上一次的滚动位置。

ref.current←value(不触发 render)\displaystyle \text{ref}.\text{current} \leftarrow \text{value} \quad (\text{不触发 render})
赋值给 ref.current 相当于在便签上写一笔:信息更新了,但不需要惊动整个门店。
  1. 创建 ref 对象useRef 返回一个稳定的容器对象。
    ref=useRef(initial)\text{ref} = \text{useRef}(\text{initial})
  2. 读写 current通过 current 属性存取可变值。
    ref.current\text{ref}.\text{current}
  3. 对比 useState修改 current 不会触发重渲染,这是它与 state 的本质区别。
    Δref⇏re-render\Delta \text{ref} \nRightarrow \text{re-render}

自定义Hook封装

总部发现各家分店都在做同一套“早高峰备料”动作:检查豆仓、磨两批豆、预热机器、摆好杯具。于是总部把这套动作整理成一张标准作业卡,印上编号 SOP-101,让各家店直接领取使用。

当多个组件复用相同的状态逻辑(如“表单输入 + 校验 + 提交”),React 的最佳实践是提取自定义 Hook。它本质是一个函数,名称以 use 开头,内部可调用任意内置 Hook。

自定义 Hook 复用的是“逻辑”而非“渲染”。它将 useState、useEffect、useReducer 等组合封装,返回所需的状态和操作;它不返回 JSX,只返回数据。以 use 开头命名的意义在于:ESLint 插件能正确识别并检查 Hook 调用规则。

SOP-101 卡片的威力在于“拷贝到每家分店,风味保持一致”。自定义 Hook 也一样:一份逻辑、多处复用,团队里每个成员都能像看标准作业卡一样迅速理解。这正是 Hooks 设计哲学的终点——把复杂性装进工具包,把简单留给使用者。

useXxx(params)→[state, actions]\displaystyle \text{useXxx}(\text{params}) \to \left[\text{state},\ \text{actions}\right]
自定义 Hook 把“一组状态 + 一组操作”打包输出,像标准作业卡一样即领即用。
  1. 命名约定以 use 开头的函数会被工具链当作 Hook 检查。
    useXxx(⋅)\text{useXxx}(\cdot)
  2. 组合原语内部通过函数的复合,将多个内置 Hook 编排为一个逻辑单元。
    g=h1∘h2∘h3g = h_1 \circ h_2 \circ h_3
  3. 返回结果按需暴露状态与操作,供组件解构使用。
    [state, actions]\left[\text{state},\ \text{actions}\right]

AI 应用场景

AI Chat 组件的流式消息管理

阶段:前端交互层

解决的问题:传统类组件管理 AI 对话消息列表时,this.setState 在流式回调中容易读到过期状态,代码割裂、状态混乱。AI 的流式响应需要“边接收边更新”。

具体案例:一个 AI 对话应用,用 useState 保存 messages 数组;用 useRef 保存 AbortController 实例,实现“停止生成”;用 useEffect 监听 WebSocket 流式响应,增量更新消息。三个 Hook 各司其职,代码量比类组件减少一半以上,且销毁组件时会自动清理监听。

数据可视化大屏的渲染优化

阶段:数据处理与展示

解决的问题:大模型产出的 JSON 日志需要实时解析、聚合、渲染,每次重渲染都重新计算会导致页面掉帧。

具体案例:一个 AI 训练监控大屏,用 useMemo 缓存统计指标(如 loss 均值、吞吐量),避免每次渲染都遍历日志;用 useCallback 稳定图表组件的回调函数,让 Canvas 图表库跳过无意义的子组件重渲染;用 useEffect 订阅数据流并批量写入 state。

多 Agent 编排工作台

阶段:Agent 状态管理

解决的问题:多个 Agent 的状态(pending/running/done)与任务队列共享逻辑复杂,每个面板各写一套状态机容易失控。

具体案例:基于自定义 Hook useAgentState 封装 useReducer + useEffect,统一管理 Agent 状态流转、超时重试、结果回写。多个面板共用一个 Hook,状态逻辑完全一致,新成员看代码如同看 SOP 卡片。

Python 演示

python44 行
"""用 Python 模拟 React Hooks 的核心运行机制"""  class FakeReact:    """极简 Hooks 运行时:模拟 useState 与渲染循环"""     def __init__(self):        self._states = {}          # hook名 -> 当前值        self._render_queue = 0     # 待渲染次数        self._render_count = 0     # 已渲染次数     def useState(self, hook_name, initial):        if hook_name not in self._states:            self._states[hook_name] = initial         def set_state(value):            self._states[hook_name] = value            self._render_queue += 1            print("  [setState] %s = %s" % (hook_name, value))         return self._states[hook_name], set_state     def render(self, component):        while True:            view = component()            self._render_count += 1            print("  渲染 #%d: %s" % (self._render_count, view))            if self._render_queue == 0:                break            self._render_queue -= 1        return view  def Counter(fr):    count, set_count = fr.useState("count", 0)    if count < 3:        set_count(count + 1)    return "<h1>计数: %d</h1>" % count  if __name__ == "__main__":    fr = FakeReact()    print("=== 函数组件渲染闭环演示 ===")    fr.render(lambda: Counter(fr))
目的:用 Python 类模拟 React 中 useState 的“声明-更新-重渲染”闭环,展示函数组件如何在状态更新后反复执行自身。
  1. self._states = {}建立一个“状态仓库”,保存每个 hook 名对应的当前值。
  2. def useState(self, hook_name, initial):模拟 useState 的声明入口,首次调用时初始化。
  3. self._states[hook_name] = initial只有当 hook 首次出现时才写入初始值,后续渲染不再重置。
  4. self._render_queue += 1set_state 每次更新都会让“待渲染次数”加一,类似 React 的调度标记。
  5. while True:只要队列里还有待渲染任务,就不断重新执行组件函数——这就是“重渲染”的本质。
  6. if count < 3: set_count(count + 1)组件内部连续触发三次状态更新,展示批处理式的渲染闭环。

常见误区

误区类组件是 React 的老牌组件,Hooks 是新功能,那我在类组件里加一行 useState 应该也能工作吧?

正解Hooks 的设计前提是“函数组件的渲染是一次干净的函数调用”,React 靠“每次渲染按相同顺序调用 Hook”来关联状态。类组件有实例、this 和生命周期方法,与 Hooks 的调用顺序机制完全不兼容,强行使用会导致 React 直接报错。类组件想管状态继续用 this.state,想复用逻辑可以改用高阶组件,或逐步改造为函数组件。

为何常见:初学者看到函数组件能用 useState,容易以为它是“对类组件功能的补充”,而忽略了两种组件模型的根本差异。

误区副作用函数的第二个参数传空数组,那就说明这个副作用永远不会运行。

正解空依赖数组的正确含义是“这个副作用不依赖任何状态,因此只在组件首次挂载完成后执行一次”,之后即使重渲染也不会再运行。如果效果里需要读取最新状态而依赖为空,就会遭遇“过期闭包”。请求初始化数据时 [] 很常用,但“只执行一次”和“永不执行”完全是两回事。

为何常见:把“依赖”误解为“触发条件”,忽略了依赖数组的本质是“变化检测条件”。

误区用 useMemo 包装后,这个计算结果就一定不会重复执行了,那我把请求副作用放进去应该也没问题。

正解useMemo 只是告诉 React“这个值值得缓存”,React 在内存压力或组件卸载等场景下可能丢弃缓存。官方文档明确指出:useMemo 是性能优化,不是语义保证。所有副作用必须放在 useEffect 里,绝不能写进 useMemo 的计算函数。

为何常见:把“缓存”想象成数据库持久化,忽略了 React 调度器拥有自主决定何时丢弃缓存的权力。

误区所有“状态”都该用 useRef 存,因为它保存的值可以随意修改,改完界面也应该跟着变。

正解useRef 的核心特征恰恰是“修改 current 不触发重渲染”。它的 .current 属性是一个稳定容器,适合存放定时器 ID、DOM 节点、旧值快照等不需要驱动 UI 的数据。如果界面需要随值变化,必须用 useState 或 useReducer。

为何常见:从“能保存值”推导出“能驱动界面”,混淆了“存储”与“状态更新”两个概念。

面试题

Q1. React Hooks 为什么只能在函数组件中调用?

单选题 · easy

  • A.函数组件性能比类组件更高
  • B.Hooks 依赖函数调用顺序管理状态← 正确答案
  • C.类组件不支持异步更新功能
  • D.类组件已经自带状态管理机制

答案:B。Hooks 通过“每次渲染按相同顺序调用”来关联状态。函数组件的渲染是一次完整函数执行,React 可以在每次调用时重置 hook 索引;而类组件有实例和 this,并不适用这种模型。性能、异步、自带状态都不是根本原因。

Q2. useState 的更新函数被连续调用两次,React 会怎么做?

单选题 · easy

  • A.执行两次渲染丢弃前一次结果
  • B.批量合并为一次最终渲染← 正确答案
  • C.立即同步更新页面 DOM
  • D.对多次更新直接抛出异常

答案:B。React 18 的自动批处理会将同一事件循环内的多次 setState 合并为一次渲染。合并不是丢弃,而是统一应用全部更新;同步更新是旧版 React 的个别行为;多次更新也不会抛异常。

Q3. useEffect 依赖数组为 [count] 时,下列说法正确的是?

单选题 · mid

  • A.count 变化时先执行上次清理函数再执行新效果← 正确答案
  • B.只有组件卸载时才会执行清理函数
  • C.每次渲染 count 不变也会再次执行效果
  • D.效果只会在 count 首次变为非零时执行

答案:A。当依赖变化触发副作用重跑时,React 会先运行上一次 effect 返回的清理函数,再执行新的 effect。卸载时的清理只是其中一环;count 不变时 React 会对比依赖并跳过执行。

Q4. useMemo 最适合解决下面哪类问题?

单选题 · mid

  • A.避免子组件因父组件重渲染而重渲染
  • B.缓存昂贵的计算结果避免重复计算← 正确答案
  • C.在组件渲染期之外发起网络请求
  • D.跨组件层级共享全局主题变量

答案:B。useMemo 的核心用途是“缓存计算值”。避免子组件重渲染通常靠 memo + useCallback;网络请求属于副作用归 useEffect 管;跨层共享数据用 useContext。

Q5. useCallback 与 useMemo 的核心区别是什么?

单选题 · mid

  • A.一个缓存函数引用,另一个缓存计算结果← 正确答案
  • B.一个接收依赖数组,另一个不接收
  • C.一个用于事件处理,另一个用于数据请求
  • D.一个只能在组件顶层,另一个可以在循环内

答案:A。useCallback(fn, deps) 缓存的是函数本身,useMemo(() => compute(), deps) 缓存的是计算结果。两者都需要依赖数组,且都只能在组件顶层调用。

Q6. 关于 React 闭包陷阱,描述最准确的是?

单选题 · hard

  • A.组件卸载后 setState 导致内存泄漏告警
  • B.异步回调读取到了旧渲染中的过期状态值← 正确答案
  • C.两个 useEffect 互相依赖导致无限循环
  • D.ref.current 在多处修改产生竞态条件

答案:B。闭包陷阱指异步回调(如 setTimeout、Promise)捕获的是它被创建那一刻的渲染快照,后续 setState 更新后回调仍持有旧值。内存泄漏、无限循环、竞态是不同问题,不属于“闭包陷阱”的准确定义。

Q7. useContext 的作用是什么?

单选题 · easy

  • A.在任意后代组件中读取上层共享数据← 正确答案
  • B.为组件提供局部状态与更新函数
  • C.缓存跨层级传递的复杂对象引用
  • D.替代 props 完成所有数据绑定功能

答案:A。useContext 让组件从最近的 Provider 读取共享值,解决深层透传问题。局部状态是 useState 的职责;缓存引用是 useMemo/useCallback 的职责;它不能完全替代 props。

Q8. 下列哪些 Hook 使用方式会违反 React 的 Hook 规则?

多选题 · mid

  • A.在 for 循环内部调用 useState← 正确答案
  • B.在自定义 Hook 内部调用 useEffect
  • C.在条件语句中提前返回后再调用 useMemo← 正确答案
  • D.在事件处理函数中调用 useCallback← 正确答案

答案:ACD。Hook 规则要求“只在顶层调用”,循环、条件、嵌套函数都会破坏调用顺序的稳定性。事件处理函数不是函数组件,调用 Hook 同样违规。自定义 Hook 内部调用内置 Hook 是合法用法。

Q9. 下列关于 useRef 的描述,正确的有哪些?

多选题 · mid

  • A.修改 ref.current 不会触发组件重渲染← 正确答案
  • B.ref 对象在组件整个生命周期内保持稳定← 正确答案
  • C.可以通过 ref 直接读取任意组件内部状态
  • D.ref.current 在并发渲染中可以安全地随意共享

答案:AB。useRef 的容器对象引用稳定,修改 current 不触发渲染,适合放定时器、DOM 节点。C 错在 ref 不能直接读组件内部状态(除非显式转发);D 错在并发渲染中共享可变 ref 可能读到中间态,并不安全。

Q10. 自定义 Hook 的设计原则包括哪些?

多选题 · hard

  • A.名称以 use 开头以便静态检查工具识别← 正确答案
  • B.内部可以自由组合多个内置 Hook← 正确答案
  • C.必须返回一个数组供组件解构使用
  • D.每个 Hook 内部最多只能调用一次 useState

答案:AB。自定义 Hook 的本质是“逻辑复用的函数”,必须以 use 开头,内部可组合任意内置 Hook。返回结构没有强制格式,可以是数组、对象乃至单个值;一个 Hook 内调用多少次 useState 完全由业务决定。