React Hooks 进阶篇:useMemo / useCallback / useReducer / useImperativeHandle,用「算账 + 审批」讲透
💡 承接基础篇。基础篇讲了 4 个基础 Hook(useState / useEffect / useRef / useContext),这一篇讲 4 个进阶 Hook。它们是性能优化和面试的高频考点,同样用生活比喻讲透。
一、先记住进阶篇的四个"工具"
| Hook | 比喻 | 一句话作用 |
|---|---|---|
useMemo |
🧮 账本 | 算过的账别重算 |
useCallback |
📇 固定名片 | 别每次渲染都换手机号 |
useReducer |
📋 审批中心 | 改状态走流程,集中处理 |
useImperativeHandle |
🎛️ 受控按钮 | 给子组件开几个指定开关 |
二、useMemo:把算过的账记在账本上
1. 痛点:每次渲染都在"白算一遍"
先看这段代码:
jsx
function TodoList({ todos }) {
const [keyword, setKeyword] = useState('');
// ❌ 每次渲染都会重新执行 filter,哪怕 keyword 根本没变
const filtered = todos.filter(t => t.title.includes(keyword));
return <ul>{filtered.map(t => <li key={t.id}>{t.title}</li>)}</ul>;
}
假设 todos 有一万条,你只是在输入框里敲了一个字,组件重新渲染,这 1 万条数据就要从头过滤一遍------哪怕过滤结果其实没变。
这就像你每次进门都要重新心算一遍这个月的水电费总额,明明"单价"和"用量"都没变,纯属浪费脑细胞。
2. 解法:用 useMemo 把结果"记下来"
jsx
import { useMemo, useState } from 'react';
function TodoList({ todos }) {
const [keyword, setKeyword] = useState('');
// ✅ 只有 todos 或 keyword 变化时,才重新计算;否则直接返回上次的结果
const filtered = useMemo(() => {
console.log('正在过滤...'); // 只有真正重算时才会打印
return todos.filter(t => t.title.includes(keyword));
}, [todos, keyword]); // 👈 依赖数组
return <ul>{filtered.map(t => <li key={t.id}>{t.title}</li>)}</ul>;
}
useMemo 的写法拆开看:
jsx
const 结果 = useMemo(() => 计算函数, [依赖]);
- 第一次渲染:执行计算,存下结果;
- 之后渲染:依赖没变 → 直接返回缓存的结果,计算函数根本不执行;
- 依赖变了 → 重新计算。
3. 另一个隐藏用途:保持"引用稳定"
useMemo 不只是为了省计算,还常用来让一个对象/数组的引用保持不变 (配合 React.memo 时很重要):
jsx
// ❌ 每次渲染都 new 一个新对象,引用变了
const config = { theme: 'dark', size: 'large' };
// ✅ 只有依赖变化时,才产生新对象,否则一直用同一个引用
const config = useMemo(
() => ({ theme: 'dark', size: 'large' }),
[]
);
⚠️ 别滥用 :如果计算本身很便宜(比如过滤 3 条数据),用
useMemo反而增加了"比较依赖 + 缓存管理"的开销,可能更慢。先测量、再优化。
三、useCallback:别每次渲染都"换手机号"
1. 一个隐蔽的坑:函数每次渲染都是"新函数"
函数组件里定义的函数,每次渲染都会重新创建,引用地址会变:
jsx
function Parent() {
const handleClick = () => console.log('clicked');
// 这个函数每次渲染都是一个新的,引用不一样
}
平时没事,但当这个函数被传给一个用 React.memo 包裹的子组件时,问题就来了------子组件会因为"函数引用变了"而白白重渲染:
jsx
import { memo, useState } from 'react';
// memo 包裹:props 不变,子组件就不重渲染
const Child = memo(function Child({ onClick }) {
console.log('子组件渲染了');
return <button onClick={onClick}>点我</button>;
});
function Parent() {
const [count, setCount] = useState(0);
// ❌ 每次父组件渲染,都产生新函数 → Child 的 props 变了 → 跟着重渲染
const handleClick = () => console.log('clicked');
return (
<>
<Child onClick={handleClick} />
<button onClick={() => setCount(c => c + 1)}>父组件 +1:{count}</button>
</>
);
}
你点父组件的按钮时,会看到控制台不停打印"子组件渲染了"------可子组件跟 count 半毛钱关系都没有,纯属被误伤。
2. 解法:用 useCallback 把函数"固定住"
jsx
import { memo, useCallback, useState } from 'react';
const Child = memo(function Child({ onClick }) {
console.log('子组件渲染了');
return <button onClick={onClick}>点我</button>;
});
function Parent() {
const [count, setCount] = useState(0);
// ✅ 缓存函数:依赖 [] 不变,永远返回同一个函数引用
const handleClick = useCallback(() => {
console.log('clicked');
}, []);
return (
<>
<Child onClick={handleClick} />
<button onClick={() => setCount(c => c + 1)}>父组件 +1:{count}</button>
</>
);
}
现在父组件更新 count 时,handleClick 的引用没变,Child 的 props 没变,memo 就会帮它"偷懒"不重渲染。
3. useMemo 和 useCallback 到底啥区别?
这是面试必考题,其实一句话:
useCallback(fn, deps)本质上就是useMemo(() => fn, deps)。
useMemo缓存的是函数的返回值(一个值)useCallback缓存的是函数本身
就像:useMemo 是把"算出来的结果"记在账本上,useCallback 是把"那个会算账的人"固定下来,别每次换人。
⚠️ 同样别滥用 :单独用
useCallback而不配合React.memo,基本是"优化了个寂寞",还多了一层开销。
四、useReducer:把「改状态」送进审批中心
1. 什么时候 useState 不够用了
useState 很轻量,但当状态逻辑复杂时,代码会变得难维护:
jsx
// ❌ 多个状态互相纠缠,set 散落各处,逻辑不清
const [count, setCount] = useState(0);
const [step, setStep] = useState(1);
const [history, setHistory] = useState([]);
// 加一减一、重置、步进......逻辑全堆在事件里
useReducer 就是为"复杂的状态逻辑 "准备的:把所有"怎么改状态"的规则,集中到一个 reducer 函数里。
2. 核心概念:dispatch 一个"动作",reducer 负责处理
jsx
import { useReducer } from 'react';
// ① reducer:接收「当前状态 + 动作」,返回「新状态」(纯函数)
function reducer(state, action) {
switch (action.type) {
case 'increment':
return { count: state.count + 1 };
case 'decrement':
return { count: state.count - 1 };
case 'reset':
return { count: action.payload }; // payload 携带数据
default:
return state;
}
}
function Counter() {
// ② 传入 reducer 和初始状态
const [state, dispatch] = useReducer(reducer, { count: 0 });
return (
<>
<p>计数:{state.count}</p>
<button onClick={() => dispatch({ type: 'increment' })}>+1</button>
<button onClick={() => dispatch({ type: 'decrement' })}>-1</button>
<button onClick={() => dispatch({ type: 'reset', payload: 0 })}>重置</button>
</>
);
}
拆解三个核心:
| 概念 | 是什么 | 比喻 |
|---|---|---|
reducer |
纯函数 (state, action) => newState |
审批中心的规则表 |
action |
{ type, payload } |
你填的"审批单" |
dispatch |
派发动作的函数 | 把审批单投进信箱 |
3. 什么时候选 useReducer?
- 多个状态相互关联、需要一起变(比如表单里十几个字段联动)
- 状态更新逻辑复杂(一堆
if/switch堆在事件里) - 逻辑需要复用或写单元测试
如果只是"一个数字 +1",老老实实用 useState 就好,杀鸡别用牛刀。
五、useImperativeHandle:给子组件开几个「受控按钮」
1. 痛点:父组件想"命令"子组件做事
默认情况下,ref 只能拿到原生 DOM 元素 。如果你给一个自定义组件传 ref,是拿不到它的内部方法的:
jsx
const Child = (props) => {
return <input />;
};
function Parent() {
const childRef = useRef(null);
// ❌ 拿不到 Child 的内部输入框,没法主动 .focus()
return <Child ref={childRef} />;
}
有些场景父组件确实需要主动"命令"子组件:聚焦输入框、滚动到某个位置、触发动画、调子组件的方法......
这时就要用 forwardRef + useImperativeHandle 组合拳。
2. 解法:子组件"开几个指定按钮"给父组件按
jsx
import { forwardRef, useImperativeHandle, useRef } from 'react';
// ① forwardRef:让子组件能接收 ref
const Input = forwardRef(function Input(props, ref) {
const inputRef = useRef(null);
// ② useImperativeHandle:决定对外暴露哪些方法
useImperativeHandle(ref, () => ({
focus: () => inputRef.current.focus(),
clear: () => { inputRef.current.value = ''; },
}));
return <input ref={inputRef} />;
});
function Parent() {
const childRef = useRef(null);
return (
<>
<Input ref={childRef} />
<button onClick={() => childRef.current.focus()}>聚焦</button>
<button onClick={() => childRef.current.clear()}>清空</button>
</>
);
}
比喻:子组件是一个黑盒 ,useImperativeHandle 就是"在盒子上开几个指定按钮"。父组件只能按这几个按钮(focus、clear),盒子内部的东西碰不到------这样既给了控制权,又保护了内部实现。
⚠️ 尽量少用 :能用
props通信就别用它。它让数据流变得"隐晦",不好追踪,容易写出难以维护的代码。只有聚焦、滚动、动画这种"命令式操作"才值得用它。
六、一张表收下全部重点
| Hook | 解决什么 | 一句话记忆 |
|---|---|---|
useMemo |
缓存计算结果 | 算过的账别重算 |
useCallback |
缓存函数引用 | 别每次换手机号 |
useReducer |
复杂状态逻辑 | 改状态走审批 |
useImperativeHandle |
暴露子组件方法 | 开几个受控按钮 |
七、写在最后
进阶篇的四个 Hook,本质都在解决**"性能"和"复杂逻辑"**两件事:
useMemo/useCallback:让组件别做无谓的重算和重渲染;useReducer:让复杂状态变得有章可循;useImperativeHandle:在极少数命令式场景里,安全地暴露子组件能力。
不过要牢记一条铁律:它们都是"锦上添花",不是"雪中送炭"。先保证代码写对、写清晰,再考虑优化------过度优化是 React 新手最容易踩的坑之一。
📝 本文基于 React 16.8+,适用于 React 18 / 19。 🔗 官方文档:react.dev/reference/r...