useCallback 与 useMemo
一句话定义:useCallback 缓存函数引用,useMemo 缓存计算结果——它们解决的是同一个问题:引用稳定性。
一、为什么需要这两个 Hook?
React 组件每次重渲染,函数体都会从头到尾重新执行一遍。这意味着:
function Parent() {
const [count, setCount] = useState(0);
const [text, setText] = useState("");
// 每次渲染都重新创建!
const handleClick = () => {
console.log("clicked");
};
const config = { theme: "dark", lang: "zh" };
return (
<div>
<input value={text} onChange={e => setText(e.target.value)} />
<button onClick={() => setCount(count + 1)}>{count}</button>
<Child onClick={handleClick} config={config} />
</div>
);
}
text 变了 → Parent 重渲染 → handleClick 和 config 重新创建 → 传给 Child 的 props 引用变了 → Child 也跟着重渲染。
但 handleClick 和 config 的内容明明没变!子组件完全不需要重新渲染。
这就是问题所在:JavaScript 的引用相等性。
1.1 核心问题:引用相等性
{} === {} // false —— 两个空对象,但不是同一个引用
() => {} === () => {} // false —— 两个空函数,但不是同一个引用
const obj = {}
const fn = () => {}
obj === obj // true —— 同一个引用
fn === fn // true —— 同一个引用
每次渲染创建的新函数/新对象,和上次创建的内容一样,但引用不同。React 的浅比较(Object.is)只看引用,不看内容——引用变了,就认为 props 变了。
类比:你每天穿同款衣服上班,同事还是认得出你。但 JavaScript 不认”同款”,只认”是不是同一件”——换了一件新的,即使一模一样,它也认为你变了。
1.2 什么时候引用不稳定会出问题?
不是所有引用不稳定都值得修复。只有当引用变了导致不必要的重渲染时,才是问题:
// 场景 1:传给 memo 子组件 → 引用变了 → memo 失效 → 子组件白渲染
<MemoChild onClick={handleClick} />;
// 场景 2:引用作为 useEffect 依赖 → 引用变了 → effect 白执行
useEffect(() => {
fetchData(filter);
}, [filter]);
如果子组件没用 React.memo,或者引用不是 effect 依赖,那引用变不变都无所谓——反正子组件本来就会跟着父组件渲染。
二、useCallback:缓存函数引用
2.1 语法
const cachedFn = useCallback(fn, deps);
// ↑ 要缓存的函数 ↑ 依赖数组
2.2 做了什么
- deps 不变 → 返回上次的同一个函数引用
- deps 变了 → 返回新创建的函数
function Parent() {
const [count, setCount] = useState(0);
const [text, setText] = useState("");
// text 变了 → Parent 重渲染 → 但 handleClick 引用不变!
const handleClick = useCallback(() => {
console.log("clicked");
}, []); // 空依赖 → 永远返回同一个函数
return (
<div>
<input value={text} onChange={e => setText(e.target.value)} />
<button onClick={() => setCount(count + 1)}>{count}</button>
<MemoChild onClick={handleClick} />
</div>
);
}
类比:useCallback 像名片——给别人的永远是同一张名片,而不是每次见面重新印一张。别人(子组件)一看,名片没换,就知道你还是你。
2.3 依赖数组怎么写
依赖数组里的值变了,函数才会重新创建:
function SearchBar({ projectId }) {
const [query, setQuery] = useState("");
// projectId 或 query 变了 → 重新创建函数(闭包里要用新值)
const handleSearch = useCallback(() => {
fetchResults(projectId, query);
}, [projectId, query]); // 依赖了什么就写什么
return <MemoResults onSearch={handleSearch} />;
}
规则和 useEffect 的依赖数组完全一样:函数里用到了哪些外部变量,就写进 deps。漏写了 → 闭包里读到旧值(闭包陷阱,见 useState完全指南 §5.1)。
2.4 什么时候用 / 不用 useCallback
| 场景 | 用不用 | 原因 |
|---|---|---|
回调传给 React.memo 子组件 | ✅ 用 | 引用稳定 → memo 才能生效 |
回调作为 useEffect / useMemo 依赖 | ✅ 用 | 引用稳定 → 避免反复执行 |
| 内联事件处理器传给原生 DOM | ❌ 不用 | 原生标签没有 memo,引用变不变都一样 |
| 只在组件内自用的函数 | ❌ 不用 | 没人关心它的引用 |
子组件没用 React.memo | ❌ 不用 | 子组件本来就会跟着父组件渲染 |
// ✅ 传给 memo 子组件
const MemoList = React.memo(function List({ onSelect }) {
return items.map(item => (
<li key={item.id} onClick={() => onSelect(item.id)} />
));
});
function Parent() {
const handleSelect = useCallback(id => {
setSelected(id);
}, []); // 依赖为空 → 引用永远不变 → MemoList 不重渲染
return <MemoList onSelect={handleSelect} />;
}
// ✅ 作为 useEffect 依赖
function DataFetcher({ userId }) {
const [data, setData] = useState(null);
const fetchUser = useCallback(() => {
fetch(`/api/users/${userId}`)
.then(r => r.json())
.then(setData);
}, [userId]); // userId 变了才重新创建
useEffect(() => {
fetchUser();
}, [fetchUser]); // fetchUser 引用稳定 → effect 不会反复触发
return <div>{data?.name}</div>;
}
// ❌ 没必要——原生 <input> 不做 props 浅比较
function Form() {
const [value, setValue] = useState("");
// ✅ 直接内联就行
return <input value={value} onChange={e => setValue(e.target.value)} />;
}
判断标准:这个函数的引用会不会被别人”观察”?如果会(传给 memo 子组件、作为 effect 依赖),就用;如果不会,就不用。
三、useMemo:缓存计算结果
3.1 语法
const cachedValue = useMemo(() => computeValue, deps);
// ↑ 计算函数(返回要缓存的值) ↑ 依赖数组
注意:useMemo 的第一个参数是工厂函数(() => 值),不是值本身。
3.2 做了什么
- deps 不变 → 返回上次的缓存值
- deps 变了 → 重新执行计算函数,返回新值
function ProductList({ products, filterText }) {
// filterText 变了才重新过滤
const filteredProducts = useMemo(() => {
return products.filter(p =>
p.name.toLowerCase().includes(filterText.toLowerCase())
);
}, [products, filterText]);
return <MemoList items={filteredProducts} />;
}
类比:useMemo 像备忘录——算过的结果记下来,下次同样的输入直接用,不用重新算。输入变了才重新算一遍。
3.3 什么时候用 / 不用 useMemo
| 场景 | 用不用 | 原因 |
|---|---|---|
| 计算结果作为子组件 props | ✅ 用 | 引用稳定 → memo 才能生效 |
计算结果作为 useEffect 依赖 | ✅ 用 | 引用稳定 → effect 不会反复执行 |
| 昂贵计算 | ✅ 用 | 避免每次渲染重复计算 |
| 简单计算(加法、字符串拼接) | ❌ 不用 | 计算成本 < 缓存成本 |
| 每次渲染都需要新引用 | ❌ 不用 | 缓存了反而坏事 |
// ✅ 计算结果传给 memo 子组件
function Dashboard({ data, selectedId }) {
const selectedItems = useMemo(() => {
return data.filter(item => item.categoryId === selectedId);
}, [data, selectedId]); // 不用 useMemo → 每次渲染创建新数组 → memo 失效
return <MemoChart items={selectedItems} />;
}
// ✅ 昂贵计算
function DataViz({ rawPoints }) {
// 10000 个点的聚类计算,每次渲染都跑一遍太浪费
const clusters = useMemo(
() => expensiveClusterAlgorithm(rawPoints),
[rawPoints]
);
return (
<svg>
{clusters.map(c => (
<Circle key={c.id} {...c} />
))}
</svg>
);
}
// ❌ 简单计算不需要缓存
function Greeting({ firstName, lastName }) {
// ❌ 字符串拼接不费性能,缓存反而多了 deps 比较的开销
const fullName = useMemo(
() => `${firstName} ${lastName}`,
[firstName, lastName]
);
// ✅ 直接算——字符串是原始类型,每次结果一样,不会导致 memo 失效
const fullName = `${firstName} ${lastName}`;
}
判断标准:这个值的引用会不会被别人”观察”?计算是否昂贵?两个条件满足任一就用,都不满足就不用。
四、React.memo 配合:三者联合作战
4.1 React.memo 做了什么
React.memo 是高阶组件,对组件的 props 做浅比较——如果每个 prop 的引用都没变,就跳过重渲染:
const MemoChild = React.memo(function Child({ onClick, config }) {
// 只有 onClick 或 config 的引用变了,才会重新渲染
return <div onClick={onClick}>{config.label}</div>;
});
4.2 不用 useCallback/useMemo → memo 失效
function Parent() {
const [count, setCount] = useState(0);
// 每次渲染都创建新函数和新对象
const handleClick = () => {
/* ... */
};
const config = { label: "Click me" };
// count 变了 → Parent 重渲染 → handleClick 和 config 引用变了
// → MemoChild 的 props 引用变了 → 浅比较失败 → memo 失效 → 白渲染
return (
<div>
<button onClick={() => setCount(c => c + 1)}>{count}</button>
<MemoChild onClick={handleClick} config={config} />
</div>
);
}
4.3 三者配合的完整模式
const MemoChild = React.memo(function Child({ onClick, items }) {
console.log("Child rendered"); // 只在 props 真正变化时打印
return (
<ul>
{items.map(item => (
<li key={item.id} onClick={() => onClick(item.id)}>
{item.name}
</li>
))}
</ul>
);
});
function Parent({ projectId }) {
const [count, setCount] = useState(0);
const [items, setItems] = useState([]);
// ✅ useCallback 稳定函数引用
const handleSelect = useCallback(id => {
console.log("Selected:", id);
}, []); // 无外部依赖 → 引用永远不变
// ✅ useMemo 稳定计算结果引用
const visibleItems = useMemo(() => {
return items.filter(item => item.projectId === projectId);
}, [items, projectId]);
return (
<div>
<button onClick={() => setCount(c => c + 1)}>{count}</button>
{/* count 变了 → Parent 重渲染 → 但 handleSelect 和 visibleItems 引用不变 */}
{/* → MemoChild 的 props 没变 → memo 生效 → Child 不重渲染 */}
<MemoChild onClick={handleSelect} items={visibleItems} />
</div>
);
}
完整模式:
React.memo守门(浅比较 props),useCallback稳定函数引用,useMemo稳定对象/数组引用。三者缺一——memo 就可能失效。
五、常见过度优化陷阱
5.1 过度使用 useMemo 缓存简单值
function UserCard({ name, age }) {
// ❌ 加法、字符串拼接——计算成本几乎为零,缓存反而多了 deps 比较的开销
const birthYear = useMemo(() => 2026 - age, [age]);
const displayName = useMemo(() => name.toUpperCase(), [name]);
// ✅ 直接算,简单明了
const birthYear = 2026 - age;
const displayName = name.toUpperCase();
}
原则:计算成本 < 缓存成本(deps 比较 + 闭包维护)时,不要用
useMemo。
5.2 到处用 useCallback
function Page() {
const [text, setText] = useState("");
// ❌ 只传给原生 <button>,没人 memo 它
const handleClick = useCallback(() => {
setText("");
}, []);
// ❌ 只在组件内自用
const formatText = useCallback(t => t.trim().toLowerCase(), []);
// ✅ 直接写,代码更简洁
return (
<div>
<input value={text} onChange={e => setText(e.target.value)} />
<button onClick={() => setText("")}>Clear</button>
<p>{text.trim().toLowerCase()}</p>
</div>
);
}
5.3 正确的心智模型
先写正确的代码,遇到性能问题再优化,用 React DevTools Profiler 量化。
- 先写对:不用任何缓存 Hook,让代码简洁可读
- 遇到问题:用 React DevTools Profiler 找到重渲染瓶颈
- 针对性优化:只在 Profiler 证明有问题的地方加
useCallback/useMemo/React.memo
// 第一步:先写对,不管性能
function Parent() {
const [count, setCount] = useState(0);
const handleClick = () => {
/* ... */
};
return <Child onClick={handleClick} />;
}
// 第二步:Profiler 发现 Child 重渲染太频繁
// 第三步:针对性加缓存
const MemoChild = React.memo(Child);
function Parent() {
const [count, setCount] = useState(0);
const handleClick = useCallback(() => {
/* ... */
}, []);
return <MemoChild onClick={handleClick} />;
}
过早优化是万恶之源。React 的重渲染通常很快——大部分时候你不需要这些 Hook。先用 Profiler 证明有问题,再动手。
六、依赖数组与 useState setter 的特殊规则
6.1 useState setter 引用天然稳定
useState 返回的 setter 函数,引用在组件整个生命周期内不变:
function Counter() {
const [count, setCount] = useState(0);
// setCount 的引用永远不变——React 保证这一点
useEffect(() => {
const timer = setInterval(() => setCount(c => c + 1), 1000);
return () => clearInterval(timer);
}, []); // 空依赖没问题,setCount 引用稳定
}
// ❌ 多此一举——setCount 本身就是稳定的
const stableSetCount = useCallback(setCount, [])
// ✅ 直接用
<Child onChange={setCount} />
6.2 为什么 setter 是稳定的?
setter 不依赖任何外部变量——它内部用函数形式 prev => newValue 拿最新值,不需要闭包捕获。React 在内部实现时就保证了同一个 setter 引用贯穿组件生命周期。
记住:
useState的 setter、useRef的.current、dispatch(来自useReducer)——这些引用天然稳定,不需要useCallback包裹。
七、useCallback 和 useMemo 的关系
7.1 本质上是同一个东西
useCallback 是 useMemo 的语法糖:
// 这两行等价
useCallback(fn, deps);
useMemo(() => fn, deps);
useMemo 缓存的是工厂函数的返回值。当工厂函数 () => fn 返回一个函数时,缓存的就是这个函数——和 useCallback 一模一样。
7.2 什么时候用哪个
| 需求 | 用哪个 | 原因 |
|---|---|---|
| 缓存函数 | useCallback | 语义更清晰,少写一层箭头函数 |
| 缓存对象/数组/计算结果 | useMemo | useCallback 做不到 |
// 缓存函数 → 用 useCallback(更简洁)
const handleClick = useCallback(() => setSelected(id), [id]);
// 等价的 useMemo 写法(多一层箭头函数,没必要)
const handleClick = useMemo(() => () => setSelected(id), [id]);
// 缓存对象 → 只能用 useMemo
const config = useMemo(
() => ({
method: "GET",
headers: { auth: token },
}),
[token]
);
简单记忆:缓存函数用
useCallback,缓存其他一切用useMemo。
八、一张图总结
引用不稳定的问题
│
┌──────────┼──────────┐
▼ ▼ ▼
函数引用变 对象/数组引用变 昂贵计算重复
│ │ │
▼ ▼ ▼
useCallback useMemo useMemo
│ │ │
└──────────┼──────────┘
▼
引用稳定了
│
┌──────────┼──────────┐
▼ ▼ ▼
memo 生效 effect 不反复 计算不浪费
使用决策流程:
- 这个函数/值的引用会被别人”观察”吗?(传给 memo 子组件、作为 effect 依赖)
- 不会 → 不用缓存
- 会 → 继续
- 它的引用天然稳定吗?(useState setter、useRef、dispatch)
- 是 → 不用缓存
- 不是 → 用
useCallback(函数)或useMemo(其他)
九、一句话
useCallback和useMemo解决引用稳定性问题——引用变了,React.memo失效、useEffect反复执行。但大部分时候你不需要它们:先写对,用 Profiler 证明有问题,再针对性优化。useState的 setter 天然稳定,不用包。