用 useCallback 和 useMemo 优化 useEffect(或其他 Hook)的依赖项,核心目标是减少不必要的重渲染/Effect 重执行——二者分别针对“函数引用”和“计算结果(对象/数组)”做缓存,让依赖项仅在真正需要变化时才更新。
一、先理解:为什么需要优化依赖项?
React 用 Object.is 做浅比较,引用类型(函数、对象、数组)每次渲染都会生成新引用,即使内容完全相同,也会被判定为“依赖变化”,导致:
useEffect频繁执行清理+回调(比如定时器被反复创建/清除);- 子组件因 props 引用变化而不必要的重渲染。
而 useCallback(缓存函数)、useMemo(缓存计算结果)的核心作用就是:让引用类型的依赖项,仅在“核心依赖”变化时才更新引用。
二、核心用法:useCallback(优化函数依赖)
1. 基本语法
const 缓存的函数 = useCallback(
() => {
// 函数逻辑(依赖 coreDep1、coreDep2)
},
[coreDep1, coreDep2] // 仅当核心依赖变化时,才生成新的函数引用
);
- 无依赖(
[]):组件生命周期内函数引用永远不变; - 有依赖:仅依赖变化时,函数引用才更新。
2. 实战场景:优化 useEffect 的函数依赖
问题代码(每次渲染都生成新的 handleClick,导致 useEffect 反复执行):
import { useEffect, useState } from 'react';
function Demo() {
const [count, setCount] = useState(0);
// 每次渲染都创建新函数 → 引用变化
const handleClick = () => {
console.log('count:', count);
};
// 依赖 handleClick → 每次渲染都执行(清理+重建)
useEffect(() => {
const timer = setInterval(handleClick, 1000);
return () => clearInterval(timer);
}, [handleClick]); // 问题:handleClick 每次都是新引用
return <button onClick={() => setCount(c => c + 1)}>count: {count}</button>;
}
优化后(用 useCallback 缓存函数,仅 count 变化时更新引用):
import { useEffect, useState, useCallback } from 'react';
function Demo() {
const [count, setCount] = useState(0);
// 仅 count 变化时,才生成新的 handleClick 引用
const handleClick = useCallback(() => {
console.log('count:', count);
}, [count]); // 核心依赖:count
// 仅 handleClick 引用变化时(即 count 变化),才重建定时器
useEffect(() => {
const timer = setInterval(handleClick, 1000);
return () => clearInterval(timer);
}, [handleClick]); // 依赖缓存后的函数
return <button onClick={() => setCount(c => c + 1)}>count: {count}</button>;
}
- 效果:只有 count 变化时,
handleClick引用才更新,useEffect才重建定时器; - 对比:若
handleClick无核心依赖(比如不依赖 count),依赖项写[],则函数引用永远不变,useEffect仅挂载时执行一次。
三、核心用法:useMemo(优化对象/数组/计算结果依赖)
1. 基本语法
const 缓存的结果 = useMemo(
() => {
// 计算逻辑:返回对象/数组/复杂计算值(依赖 coreDep1、coreDep2)
return { a: coreDep1, b: coreDep2 };
},
[coreDep1, coreDep2] // 仅核心依赖变化时,才重新计算并生成新引用
);
- 注意:
useMemo是“计算缓存”,不是“单纯的引用缓存”——它会执行回调并缓存返回值; - 无依赖(
[]):仅首次渲染计算一次,后续引用不变。
2. 实战场景1:优化 useEffect 的对象依赖
问题代码(每次渲染生成新对象,导致 useEffect 反复执行):
import { useEffect, useState } from 'react';
function Demo() {
const [a, setA] = useState(1);
const [b, setB] = useState(2);
// 每次渲染创建新对象 → 引用变化
const config = { a, b };
// 依赖 config → 每次渲染都执行
useEffect(() => {
console.log('config 变化:', config);
}, [config]);
return <div>
<button onClick={() => setA(a+1)}>修改a</button>
<button onClick={() => setB(b+1)}>修改b</button>
</div>;
}
优化后(用 useMemo 缓存对象,仅 a/b 变化时更新引用):
import { useEffect, useState, useMemo } from 'react';
function Demo() {
const [a, setA] = useState(1);
const [b, setB] = useState(2);
// 仅 a/b 变化时,才生成新的 config 引用
const config = useMemo(() => ({ a, b }), [a, b]);
// 仅 config 引用变化时(即 a/b 变化),才执行
useEffect(() => {
console.log('config 变化:', config);
}, [config]);
return <div>
<button onClick={() => setA(a+1)}>修改a</button>
<button onClick={() => setB(b+1)}>修改b</button>
</div>;
}
2. 实战场景2:优化子组件的 props 依赖(避免子组件重渲染)
问题代码(父组件每次渲染传递新数组,子组件不必要重渲染):
// 子组件(接收数组 props)
function Child({ list }) {
console.log('Child 重渲染');
return <div>{list.join(',')}</div>;
}
// 父组件
function Parent() {
const [count, setCount] = useState(0);
// 每次渲染生成新数组 → 子组件重渲染
const list = [1, 2, 3];
return <div>
<button onClick={() => setCount(c => c + 1)}>count: {count}</button>
<Child list={list} />
</div>;
}
优化后(用 useMemo 缓存数组,子组件仅数组内容变化时重渲染):
function Parent() {
const [count, setCount] = useState(0);
// 无依赖 → 数组引用永远不变
const list = useMemo(() => [1, 2, 3], []);
return <div>
<button onClick={() => setCount(c => c + 1)}>count: {count}</button>
<Child list={list} /> {/* 仅首次渲染,后续不重渲染 */}
</div>;
}
3.实战场景3:依赖项是函数
下面是一段搜索框组件的代码
const SearchInput = () => {
const [query, setQuery] = useState('')
const [result, setResult] = useState('');
const fetchResult = (curQuery) => {
console.log('Fetching For:', curQuery);
}
useEffect(() => {
if(query){
fetchResult(query);
}
}, [query, fetchResult]);
return <input type='text' value={query} onChange={(e) => setQuery(e.target.value)} />
}
上面的代码存在什么问题呢,首先函数fetchResult定义在组件内部,这意味着每次组件渲染,都将重新生成fetchResult(引用发生变化),这就会导致useEffect每次重新渲染都会执行一遍回调。
第一种解决方案就是使用useCallback保持函数的引用。
const SearchInput = () => {
const [query, setQuery] = useState('')
const [result, setResult] = useState('');
const fetchResult = useCallback((curQuery) => {
console.log('Fetching For:', curQuery);
}, []) // 用useCallback保持函数的引用,避免重复创建
useEffect(() => {
if(query){
fetchResult(query);
}
}, [query, fetchResult]);
return <input type='text' value={query} onChange={(e) => setQuery(e.target.value)} />
}
第二种解决方法就是将函数写在useEffect内部,如果函数是一个纯函数(不涉及组件内部元素的引用),还可以直接写在组件外。这样就不会因为重新渲染,而导致函数重复创建。这种方法要注意过时闭包问题。
const SearchInput = () => {
const [query, setQuery] = useState('')
const [result, setResult] = useState('');
useEffect(() => {
const fetchResult = (curQuery) => {
console.log('Fetching For:', curQuery);
}
if(query){
fetchResult(query);
}
}, [query]);
return <input type='text' value={query} onChange={(e) => setQuery(e.target.value)} />
}
四、关键注意事项(避免滥用)
useCallback/useMemo 本身有微小的性能开销(缓存逻辑),以下场景不需要优化:
- 依赖项是基本类型(数字、字符串、布尔值):天然无需缓存,
Object.is能正确判断; - 函数/对象仅在组件内部使用,且不作为 Hook 依赖:比如仅作为点击事件回调,无需缓存;
- 计算逻辑极简单(比如
const sum = a + b):useMemo的开销可能大于重新计算的开销。
优化原则:仅当依赖项导致“明显的性能问题”(比如高频渲染的列表、复杂的 Effect 逻辑)时,才使用 useCallback/useMemo。
五、进阶:useCallback + useMemo 配合使用
当函数依赖于 useMemo 缓存的对象时,可组合使用:
function Demo() {
const [a, setA] = useState(1);
// 缓存对象
const config = useMemo(() => ({ a }), [a]);
// 缓存函数(依赖缓存后的对象)
const handleConfig = useCallback(() => {
console.log('config:', config);
}, [config]); // 仅 config 变化时,函数引用更新
useEffect(() => {
handleConfig();
}, [handleConfig]); // 仅 handleConfig 变化时执行
return <button onClick={() => setA(a+1)}>修改a</button>;
}
总结
- useCallback:缓存函数引用,解决“函数每次渲染生成新引用导致依赖变化”的问题,依赖项写函数内部用到的核心变量;
- useMemo:缓存对象/数组/计算结果,解决“引用类型每次渲染生成新引用导致依赖变化”的问题,依赖项写计算逻辑用到的核心变量;
- 优化核心:让依赖项的“引用变化”仅发生在“内容真的需要变化”时,减少不必要的 Hook 执行/组件重渲染;
- 避坑点:不滥用——仅当性能问题可感知时才使用,避免增加代码复杂度。
简单来说:useCallback 管函数,useMemo 管对象/数组/计算值,二者都是为了让依赖项“变的更懒”,只在必要时更新。