useCallback 用于缓存函数,避免在每次渲染时创建新的函数实例
scss
const [count, setCount] = useState(0);
const [name, setName] = useState('');
// ❌ 每次渲染都会创建新函数
const handleClick = () => {
console.log('Button clicked');
};
// ✅ 使用 useCallback 缓存函数
const handleClickMemo = useCallback(() => {
console.log('Button clicked');
}, []); // 依赖数组为空,函数永远不会重新创建
// ✅ 依赖变化时重新创建函数
const handleNameChange = useCallback((newName: string) => {
setName(newName);
}, [setName]); // 依赖 setName
useMemo
用于缓存计算结果,避免在每次渲染时重复执行昂贵的计算。
注意
应该只用于"纯函数"计算,即给定相同输入总是返回相同输出,且没有副作用。 因为useMemo中的内容会执行不止一次
不能用于打印日志、网络请求 、开启定时器、给外部数据赋值等等其他任何非纯计算的操作。
ini
const [count, setCount] = useState(0);
const [items, setItems] = useState<number[]>([]);
// ❌ 每次渲染都执行昂贵计算
const expensiveValue = items.reduce((sum, item) => {
// 模拟昂贵的计算
let result = 0;
for (let i = 0; i < 1000000; i++) {
result += item * i;
}
return sum + result;
}, 0);
// ✅ 使用 useMemo 缓存计算结果
const expensiveValueMemo = useMemo(() => {
return items.reduce((sum, item) => {
// 模拟昂贵的计算
let result = 0;
for (let i = 0; i < 1000000; i++) {
result += item * i;
}
return sum + result;
}, 0);
}, [items]); // 只有 items 变化时才重新计算
// ✅ 缓存过滤后的列表
const filteredItems = useMemo(() => {
return items.filter(item => item > 5);
}, [items]);
// ✅ 缓存复杂对象
const complexObject = useMemo(() => ({
data: items,
count: count,
timestamp: Date.now(),
processed: items.map(item => item * 2)
}), [items, count]);
子组件使用 React.memo 优化
javascript
const ChildComponent = React.memo(({ onClick }: { onClick: () => void }) => {
console.log('ChildComponent rendered');
return <button onClick={onClick}>Click me</button>;
});
useCallback
的主要目的是优化性能,但只有在特定条件下才能真正起到优化作用。
注意事项
1. 不要过度使用
2. 依赖数组要完整
3. 避免在 useMemo 中执行副作用
详细解释
1. 没有 React.memo 的情况
javascript
typescript
// 子组件没有使用 React.memo
const ChildComponent = ({ onClick }) => {
console.log('ChildComponent rendered'); // 每次都会打印
return <button onClick={onClick}>Click me</button>;
};
const Parent = () => {
const [count, setCount] = useState(0);
// ❌ 没有意义的 useCallback
const handleClick = useCallback(() => {
console.log('Clicked');
}, []);
// ✅ 其实和这样写效果一样
const handleClick2 = () => {
console.log('Clicked');
};
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
{/* 每次 Parent 重新渲染时,ChildComponent 都会重新渲染 */}
<ChildComponent onClick={handleClick} />
</div>
);
};
问题 :即使使用了 useCallback
,子组件仍然会在每次父组件重新渲染时重新渲染,因为:
- 父组件重新渲染时创建新的 JSX
<ChildComponent onClick={handleClick} />
被重新创建- React 认为这是一个新的元素,会重新渲染子组件
2. 使用 React.memo 的情况
javascript
typescript
// 子组件使用 React.memo
const MemoizedChild = React.memo(({ onClick }) => {
console.log('MemoizedChild rendered'); // 只有在 props 真正变化时才打印
return <button onClick={onClick}>Click me</button>;
});
const Parent = () => {
const [count, setCount] = useState(0);
// ✅ 有意义的 useCallback
const handleClick = useCallback(() => {
console.log('Clicked');
}, []);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
{/* 只有当 handleClick 真正变化时,MemoizedChild 才会重新渲染 */}
<MemoizedChild onClick={handleClick} />
</div>
);
};
效果:
- 点击 "Increment" 按钮时,count 变化,Parent 重新渲染
- 但由于
useCallback
的依赖数组为空,handleClick
引用保持不变 MemoizedChild
通过React.memo
检测到 props 没有变化,跳过重新渲染
3. 对比示例
javascript
typescript
// 演示区别
const Parent = () => {
const [count, setCount] = useState(0);
// 没有 useCallback
const handleClickWithoutCallback = () => {
console.log('Clicked');
};
// 有 useCallback
const handleClickWithCallback = useCallback(() => {
console.log('Clicked');
}, []);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
{/* 每次 Parent 重新渲染时,这个子组件都会重新渲染 */}
<RegularChild onClick={handleClickWithoutCallback} />
{/* 只有当 handleClickWithCallback 引用变化时才重新渲染 */}
<MemoizedChild onClick={handleClickWithCallback} />
</div>
);
};
什么是副作用(Side Effect)?
副作用是指函数除了返回值之外,还做了其他影响程序状态的事情。常见的副作用包括:
1. 控制台日志
ini
typescript
const result = useMemo(() => {
console.log('This is a side effect'); // ❌ 副作用
return data.filter(item => item.active);
}, [data]);
2. 修改外部变量
ini
typescript
let externalVar = 0;
const result = useMemo(() => {
externalVar++; // ❌ 副作用:修改了外部状态
return data.map(item => item.value);
}, [data]);
3. 网络请求
scss
typescript
const result = useMemo(() => {
fetch('/api/data'); // ❌ 副作用:网络请求
return processData(data);
}, [data]);
4. DOM 操作
ini
typescript
const result = useMemo(() => {
document.title = 'New Title'; // ❌ 副作用:修改 DOM
return formattedData;
}, [data]);
5. 定时器
javascript
typescript
const result = useMemo(() => {
setTimeout(() => { /* ... */ }, 1000); // ❌ 副作用:设置定时器
return calculatedValue;
}, [deps]);
为什么 useMemo 中不应该有副作用?
1. 不可预测的执行时机
javascript
typescript
const Component = () => {
const [count, setCount] = useState(0);
// ❌ 问题:这个 console.log 的执行时机不可预测
const result = useMemo(() => {
console.log('Memo executed'); // 可能执行 0 次、1 次或多次
return count * 2;
}, [count]);
return <div>{result}</div>;
};
2. React 的优化机制
scss
typescript
// React 可能会跳过 useMemo 的执行
const expensiveValue = useMemo(() => {
console.log('This might not run'); // ❌ 不可靠
return heavyCalculation();
}, [deps]);
正确的做法
使用 useEffect 处理副作用:
scss
typescript
const Component = () => {
const [data, setData] = useState([]);
const [count, setCount] = useState(0);
// ✅ 纯净的计算
const processedData = useMemo(() => {
return data.map(item => ({
...item,
processed: item.value * 2
}));
}, [data]);
// ✅ 副作用放在 useEffect 中
useEffect(() => {
console.log('Data changed:', processedData.length);
// 其他副作用操作
}, [processedData]); // 依赖处理后的数据
// ✅ 或者依赖原始数据
useEffect(() => {
console.log('Count changed to:', count);
}, [count]);
return <div>{/* 渲染内容 */}</div>;
};
实际例子:
javascript
typescript
const UserProfile = ({ userId }) => {
const [user, setUser] = useState(null);
// ✅ 纯净的数据处理
const userDisplayInfo = useMemo(() => {
if (!user) return null;
return {
displayName: `${user.firstName} ${user.lastName}`,
avatar: user.profileImage || '/default-avatar.png',
memberSince: new Date(user.createdAt).getFullYear()
};
}, [user]);
// ✅ 副作用放在 useEffect 中
useEffect(() => {
console.log('User display info updated:', userDisplayInfo);
// 可以在这里做其他副作用操作,比如发送分析事件
}, [userDisplayInfo]);
// ✅ 数据获取也应该是副作用
useEffect(() => {
const fetchUser = async () => {
const userData = await fetchUserById(userId);
setUser(userData);
};
fetchUser();
}, [userId]);
return (
<div>
{userDisplayInfo && (
<div>
<img src={userDisplayInfo.avatar} alt="Avatar" />
<h2>{userDisplayInfo.displayName}</h2>
<p>Member since {userDisplayInfo.memberSince}</p>
</div>
)}
</div>
);
};
性能成本的解释
useCallback
本身也有成本:
javascript
typescript
// useCallback 内部需要:
// 1. 检查依赖数组是否变化
// 2. 决定是否返回缓存的函数
// 3. 如果依赖变化,创建新函数
// 对于简单函数,这个成本可能比重新创建函数还高
const simpleFunction = useCallback(() => {
return a + b; // 简单计算
}, [a, b]); // 可能得不偿失
最佳实践
ini
typescript
// ✅ 什么时候使用 useCallback:
1. 子组件使用了 React.memo
2. 函数作为 props 传递给子组件
3. 函数创建成本较高(复杂逻辑)
// ❌ 什么时候不需要 useCallback:
1. 子组件没有使用 React.memo
2. 函数很简单,重新创建成本很低
3. 函数不作为 props 传递
// ✅ 实际例子:
const ExpensiveComponent = () => {
const [items, setItems] = useState([]);
// 有价值:复杂的数据处理函数传递给 memoized 子组件
const processItem = useCallback((item) => {
// 复杂的处理逻辑
return heavyProcessing(item);
}, []);
return (
<MemoizedItemList
items={items}
onProcess={processItem} // 传递给 memoized 组件
/>
);
};
总结
useCallback
只有配合React.memo
才能发挥真正的性能优化作用,否则就是过早优化,反而可能增加不必要的复杂性。- useMemo:应该只用于"纯函数"计算,即给定相同输入总是返回相同输出,且没有副作用
- useEffect:专门用于处理副作用