如果你学 React 一段时间了,肯定听过这三兄弟的名字:
useMemouseCallbackReact.memo
但你可能也有这样的疑惑:
🤔 它们都能防止无意义渲染,到底有什么区别?
🤔 为什么我用了
React.memo子组件还是重新渲染?🤔 什么时候用
useMemo,什么时候用useCallback?
别急,这篇文章会用最清晰的逻辑 + 直观的代码案例,帮你从渲染底层机制彻底吃透这三者。
🧩 一、React 性能问题的根源
1.1 为什么子组件会"无辜"重渲染?
在 React 中,每当父组件状态更新时,整个函数体会重新执行一遍:
jsx
function Parent() {
const [count, setCount] = useState(0);
const handleClick = () => console.log('clicked');
console.log('Parent 渲染');
return (
<>
<p>Count: {count}</p>
<Child onClick={handleClick} />
<button onClick={() => setCount(c => c + 1)}>+1</button>
</>
);
}
function Child({ onClick }) {
console.log('Child 渲染');
return <button onClick={onClick}>子按钮</button>;
}
执行流程:
- 点击 +1 按钮
count更新,触发Parent重新执行handleClick被重新创建(新的函数引用)- React 对比
Child的 props:onClick引用变了 - 触发
Child重新渲染 ❌
问题关键: 在 JavaScript 中,每次创建的函数都是新的引用:
javascript
const fn1 = () => {};
const fn2 = () => {};
console.log(fn1 === fn2); // false
即使函数内容完全一样,React 也会认为 props 发生了变化。
⚙️ 二、React.memo:阻止"假变化"引发的渲染
2.1 什么是 React.memo?
React.memo 是一个高阶组件(HOC) ,它会对组件的 props 进行浅比较:
jsx
const Child = React.memo(function Child({ onClick }) {
console.log('🎨 Child 渲染');
return <button onClick={onClick}>子按钮</button>;
});
工作原理:
ini
父组件重渲染
↓
React.memo 检查
↓
判断:新 props === 旧 props ?
├─ 是 → ✅ 跳过渲染,复用上次结果
└─ 否 → ❌ 执行渲染
2.2 React.memo 的局限
⚠️ 注意: React.memo 只做浅比较,对于引用类型(函数、对象、数组)的变化非常敏感:
jsx
// ❌ 每次都是新引用,React.memo 失效
<Child onClick={() => {}} />
<Child data={{ name: 'Alice' }} />
<Child list={[1, 2, 3]} />
这就是为什么我们需要 useCallback 和 useMemo。
🔁 三、useCallback:稳定函数引用
3.1 基本用法
useCallback 返回一个记忆化的函数,只有当依赖项改变时才会更新:
jsx
const handleClick = useCallback(() => {
console.log('clicked');
}, []); // 依赖为空,函数引用永远不变
语法:
javascript
const memoizedCallback = useCallback(
() => {
// 你的函数逻辑
},
[依赖项]
);
3.2 配合 React.memo 使用
jsx
const Child = React.memo(({ onClick }) => {
console.log('🎨 Child 渲染');
return <button onClick={onClick}>子按钮</button>;
});
function Parent() {
const [count, setCount] = useState(0);
// ✅ 函数引用稳定,不会触发 Child 重渲染
const handleClick = useCallback(() => {
console.log('clicked');
}, []);
return (
<>
<p>Count: {count}</p>
<Child onClick={handleClick} />
<button onClick={() => setCount(c => c + 1)}>+1</button>
</>
);
}
执行结果:
- 点击 +1 →
Parent渲染 ✅ Child不再渲染 ✅(因为onClick引用未变)
3.3 带依赖的 useCallback
jsx
const [userId, setUserId] = useState(1);
const fetchUserData = useCallback(() => {
fetch(`/api/user/${userId}`);
}, [userId]); // 只有 userId 变化时才重新创建
💡 四、useMemo:缓存计算结果
4.1 与 useCallback 的区别
| 对比项 | useCallback |
useMemo |
|---|---|---|
| 缓存对象 | 函数本身 | 函数的返回值 |
| 返回值 | () => {...} |
执行结果 |
| 典型场景 | 传递给子组件的回调 | 复杂计算、派生状态 |
记忆技巧:
javascript
useCallback(fn, deps) ≈ useMemo(() => fn, deps)
4.2 实际应用场景
jsx
function ProductList({ products }) {
const [filter, setFilter] = useState('');
// ✅ 只在 products 或 filter 改变时重新计算
const filteredProducts = useMemo(() => {
console.log('🔍 执行过滤逻辑...');
return products.filter(p =>
p.name.toLowerCase().includes(filter.toLowerCase())
);
}, [products, filter]);
return (
<>
<input
value={filter}
onChange={e => setFilter(e.target.value)}
/>
{filteredProducts.map(p => (
<div key={p.id}>{p.name}</div>
))}
</>
);
}
性能对比:
| 场景 | 无 useMemo | 有 useMemo |
|---|---|---|
| 1000 个商品首次渲染 | 计算 1 次 | 计算 1 次 |
| 输入框 focus(无关状态变化) | 计算 1 次 ❌ | 0 次 ✅ |
| 修改 filter | 计算 1 次 | 计算 1 次 |
4.3 缓存对象/数组给子组件
jsx
const Child = React.memo(({ config }) => {
console.log('🎨 Child 渲染');
return <div>{config.theme}</div>;
});
function Parent() {
const [count, setCount] = useState(0);
// ✅ config 引用稳定
const config = useMemo(() => ({
theme: 'dark',
lang: 'zh-CN'
}), []);
return (
<>
<p>{count}</p>
<button onClick={() => setCount(c => c + 1)}>+1</button>
<Child config={config} />
</>
);
}
🧠 五、三者关系与配合策略
5.1 配合时序图
sequenceDiagram
participant P as 父组件
participant CB as useCallback
participant CM as useMemo
participant C as 子组件
participant M as React.memo
P->>P: state 更新
P->>CB: 检查依赖
CB-->>P: 返回缓存函数 ✅
P->>CM: 检查依赖
CM->>CM: 依赖未变,跳过计算
CM-->>P: 返回缓存值 ✅
P->>C: 传递 props
C->>M: 触发渲染前检查
M->>M: Object.is(newProps, oldProps)
alt props 引用相同
M-->>C: 跳过渲染 ✅
else props 引用不同
M->>C: 执行渲染 ❌
C-->>P: 返回 JSX
end
5.2 三者对比表
| 名称 | 类型 | 缓存内容 | 返回值 | 是否阻止渲染 | 常见搭配 |
|---|---|---|---|---|---|
React.memo |
HOC | 组件渲染结果 | 组件 | ✅ 是 | useCallback/useMemo |
useCallback |
Hook | 函数引用 | 函数 | ❌ 否 | React.memo |
useMemo |
Hook | 计算结果 | 任意值 | ❌ 否 | React.memo |
🔍 六、最佳实践与常见误区
6.1 什么时候应该用?
| 场景 | 推荐方案 | 理由 |
|---|---|---|
| 子组件接收函数 props 频繁渲染 | useCallback + React.memo |
稳定引用 + 跳过渲染 |
| 大列表过滤/排序/计算 | useMemo |
避免重复计算 |
| 传递对象/数组给 memo 子组件 | useMemo |
稳定引用 |
| 组件接收的 props 不常变化 | React.memo |
减少渲染次数 |
| 简单组件、轻量计算 | ❌ 不用 | 优化成本 > 收益 |
6.2 常见误区
❌ 误区 1:到处使用
jsx
// ❌ 过度优化,反而增加开销
const add = useCallback((a, b) => a + b, []);
const result = useMemo(() => 1 + 1, []);
正确做法: 针对性能瓶颈优化,用 React DevTools Profiler 测量。
❌ 误区 2:忘记包裹 React.memo
jsx
// ❌ 只用 useCallback 没用,子组件还会渲染
const handleClick = useCallback(() => {}, []);
<Child onClick={handleClick} /> // Child 没用 React.memo
❌ 误区 3:依赖项遗漏
jsx
// ❌ 依赖了 count 但没声明,会导致闭包陷阱
const handleClick = useCallback(() => {
console.log(count);
}, []); // 应该是 [count]
🎯 七、综合实战案例
jsx
import React, { useState, useMemo, useCallback, memo } from "react";
// 子组件:展示计算结果
const ResultDisplay = memo(({ result }) => {
console.log("🎨 ResultDisplay 渲染");
return <div>计算结果:{result}</div>;
});
// 子组件:按钮
const ActionButton = memo(({ onAction, label }) => {
console.log("🎨 ActionButton 渲染");
return <button onClick={onAction}>{label}</button>;
});
export default function App() {
const [count, setCount] = useState(0);
const [text, setText] = useState("");
// ✅ useMemo 缓存复杂计算
const expensiveResult = useMemo(() => {
console.log("💡 执行昂贵计算...");
let sum = 0;
for (let i = 0; i < 1000000; i++) {
sum += i;
}
return sum + count;
}, [count]); // 只在 count 变化时重新计算
// ✅ useCallback 缓存函数引用
const handleIncrement = useCallback(() => {
setCount(c => c + 1);
}, []);
const handleReset = useCallback(() => {
setCount(0);
}, []);
console.log("🧩 App 组件渲染");
return (
<div>
<h1>性能优化示例</h1>
<p>计数:{count}</p>
{/* 输入框变化不会触发子组件渲染 */}
<input
value={text}
onChange={e => setText(e.target.value)}
placeholder="输入文字试试"
/>
{/* 这两个组件只在必要时渲染 */}
<ResultDisplay result={expensiveResult} />
<ActionButton onAction={handleIncrement} label="+1" />
<ActionButton onAction={handleReset} label="重置" />
</div>
);
}
执行效果:
- 修改输入框 → 只有
App渲染,子组件不渲染 ✅ - 点击 +1 →
App和ResultDisplay渲染,ActionButton不渲染 ✅ - 点击重置 → 同上
🧠 八、记忆口诀(背下来你就是懂哥)
| Hook/API | 记忆口诀 | 核心功能 | 英文含义 |
|---|---|---|---|
useMemo |
"我记住值" | 缓存计算结果 | memoize value |
useCallback |
"我记住函数" | 缓存函数引用 | callback function |
React.memo |
"我记住组件" | 跳过重复渲染 | memoize component |
一句话总结:
useCallback和useMemo让 props 稳定,
React.memo让稳定的 props 不触发渲染。
📊 九、性能优化决策树
markdown
开始
│
├─ 组件渲染慢吗?
│ ├─ 否 → 不用优化
│ └─ 是 ↓
│
├─ 是子组件无意义渲染吗?
│ ├─ 是 → 用 React.memo 包裹子组件
│ │ ↓
│ │ 检查 props 类型
│ │ ├─ 函数 → useCallback
│ │ ├─ 对象/数组 → useMemo
│ │ └─ 基本类型 → 不用处理
│ │
│ └─ 否 ↓
│
└─ 有复杂计算逻辑吗?
├─ 是 → 用 useMemo 缓存结果
└─ 否 → 检查其他性能问题
🏁 十、结语
React 的性能优化不只是加几个 Hook 就完事,更重要的是理解渲染流程与引用稳定性的本质。
真正的优化思维:
- 测量优先:用 React DevTools Profiler 找到真正的瓶颈
- 针对性优化 :不是所有组件都需要
memo - 理解原理:知道为什么要用,而不是盲目照搬
- 权衡取舍:优化本身也有成本(内存、代码复杂度)