React(十五)缓存完全指南:memo、useMemo、useCallback
目标:彻底搞懂 React 19 中三个缓存 API 的作用、用法和避坑点。
本文面向课堂教学,从 React 渲染机制出发,结合生活案例和完整代码示例,帮助你判断"什么时候该用缓存"。
目录
- 为什么需要缓存?先理解 React 的渲染机制
- 缓存三兄弟速览
- React.memo:组件级缓存
- useMemo:值级缓存
- useCallback:函数级缓存
- 三兄弟对比总结
- 综合实战:商品列表页优化
- 高频问题与学生提问预判
1. 为什么需要缓存?先理解 React 的渲染机制
1.1 父组件更新,子组件默认也会重新渲染
在 React 中,组件通过 props 接收数据。当父组件的 state 或 props 发生变化时,父组件会重新渲染。而默认情况下,父组件重新渲染后,它内部所有的子组件也会跟着重新渲染。
听起来有点浪费?确实是这样。React 默认不会帮你判断"子组件的 props 到底有没有真的变",只要父组件重新渲染,子组件就会执行一次新的渲染函数。
import React, { useState } from "react";
function Parent() {
const [count, setCount] = useState(0);
return (
<div>
<p>父组件计数:{count}</p>
<button onClick={() => setCount(count + 1)}>父组件 +1</button>
{/* 父组件每次更新,Child 都会重新渲染,即使 title 没变 */}
<Child title="我是子组件" />
</div>
);
}
function Child({ title }) {
console.log("Child 重新渲染了");
return <div>{title}</div>;
}
export default Parent;
点击"父组件 +1"按钮,你会在控制台看到 Child 重新渲染了 被打印很多次。可是 Child 的 title 根本没变,它却在反复执行渲染。
1.2 重新渲染一定会导致页面重绘吗?
不一定。React 重新渲染后,会先进行 "diff" 对比:
- 如果虚拟 DOM 没变,React 不会真正操作真实 DOM(不会重绘);
- 如果虚拟 DOM 变了,React 才会更新真实 DOM。
所以即使子组件重新渲染,只要 props 和内部状态没变,真实 DOM 不一定变化。
但问题是:渲染函数本身会执行,复杂的组件执行一次可能耗时很久。这就是缓存要解决的问题。
1.3 缓存解决什么问题?
| 问题 | 说明 |
|---|---|
| 不必要的子组件渲染 | 父组件更新,子组件 props 没变,却仍然执行渲染函数 |
| 重复计算 | 每次渲染都重新执行复杂计算,比如过滤、排序、聚合 |
| 函数引用变化 | 每次渲染创建新函数,导致子组件 memo 失效或依赖项不稳定 |
缓存的核心价值:用"记忆"来减少不必要的工作,但缓存本身也有成本,不能滥用。
2. 缓存三兄弟速览
| API | 作用级别 | 官方定义通俗解释 | 核心作用 |
|---|---|---|---|
React.memo |
组件级 | 给组件包一层"记忆外套" | 如果 props 没变,跳过组件渲染 |
useMemo |
值级 | 给计算结果"记个笔记" | 依赖项没变时,不重复计算,直接返回上次结果 |
useCallback |
函数级 | 给函数"发张身份证" | 依赖项没变时,返回同一个函数引用 |
下面用三个独立章节详细讲解每个 API。
3. React.memo:组件级缓存
3.1 官方定义通俗化
React.memo 是一个高阶组件(HOC)。你可以把它理解成给组件穿了一件"记忆外套":
当组件穿上这件外套后,React 会记住它上次收到的 props。如果这次传来的 props 和上次一样,React 就直接跳过这次渲染,不再执行组件函数。
3.2 语法格式
import React, { memo } from "react";
// 方式一:导出时包裹
const MyComponent = memo(function MyComponent(props) {
return <div>{props.title}</div>;
});
export default MyComponent;
import React, { memo } from "react";
// 方式二:先定义组件,再包裹
function MyComponent(props) {
return <div>{props.title}</div>;
}
export default memo(MyComponent);
3.3 入参与返回值说明
| 参数 | 类型 | 说明 |
|---|---|---|
| 第一个参数 | 组件 | 要包裹的函数组件 |
| 第二个参数(可选) | 对比函数 | 自定义 props 比较逻辑,默认是浅比较 |
返回值:返回一个"被记忆"的新组件。
3.4 基础示例:解决父组件更新导致子组件重复渲染
import React, { useState, memo } from "react";
function Parent() {
const [count, setCount] = useState(0);
return (
<div>
<p>父组件计数:{count}</p>
<button onClick={() => setCount(count + 1)}>父组件 +1</button>
{/* Child 被 memo 包裹,title 没变时不会重新渲染 */}
<Child title="我是子组件" />
</div>
);
}
// 给 Child 穿上 memo 外套
const Child = memo(function Child({ title }) {
console.log("Child 渲染了");
return <div>{title}</div>;
});
export default Parent;
运行效果
- 没加
memo时:每次点击父组件按钮,控制台都会打印Child 渲染了。 - 加上
memo后:只有title变化时,Child才会重新渲染。
3.5 场景示例一:商品列表页的子组件缓存
假设一个商品列表页,父组件管理搜索关键词和排序方式,每个商品卡片是一个子组件。
import React, { useState, memo } from "react";
// 商品卡片组件:用 memo 包裹,只有商品数据变化时才渲染
const ProductCard = memo(function ProductCard({ product }) {
console.log("渲染商品:", product.name);
return (
<div className="product-card">
<h3>{product.name}</h3>
<p>价格:¥{product.price}</p>
</div>
);
});
function ProductList() {
const [keyword, setKeyword] = useState("");
// 假设商品列表很大
const [products] = useState([
{ id: 1, name: "苹果手机", price: 5999 },
{ id: 2, name: "华为手机", price: 4999 },
{ id: 3, name: "小米手机", price: 2999 },
]);
return (
<div>
<input
type="text"
value={keyword}
onChange={(e) => setKeyword(e.target.value)}
placeholder="搜索商品"
/>
<div className="product-list">
{products.map((product) => (
// 关键词变化时,父组件渲染,但商品卡片数据没变则不会重复渲染
<ProductCard key={product.id} product={product} />
))}
</div>
</div>
);
}
export default ProductList;
教学要点
keyword变化时,父组件重新渲染;- 因为
products数组没变,每个ProductCard的product引用也没变; memo会跳过这些子组件的渲染,提升性能。
3.6 场景示例二:带自定义对比函数的 memo
有时候默认浅比较不够用了。例如父组件传入一个对象,每次渲染都创建新对象,但对象内容其实一样。
import React, { useState, memo } from "react";
function Parent() {
const [count, setCount] = useState(0);
// 每次渲染都会创建新对象,但内容一样
const user = { name: "张三", age: 18 };
return (
<div>
<p>计数:{count}</p>
<button onClick={() => setCount(count + 1)}>+1</button>
<UserCard user={user} />
</div>
);
}
// 自定义对比函数:只比较 user.name 和 user.age
const UserCard = memo(
function UserCard({ user }) {
console.log("UserCard 渲染了");
return (
<div>
<p>姓名:{user.name}</p>
<p>年龄:{user.age}</p>
</div>
);
},
(prevProps, nextProps) => {
// 返回 true 表示"相同,不需要重新渲染"
return (
prevProps.user.name === nextProps.user.name &&
prevProps.user.age === nextProps.user.age
);
},
);
export default Parent;
自定义对比函数说明
| 参数 | 含义 |
|---|---|
prevProps |
上一次的 props |
nextProps |
新的 props |
| 返回值 | true 表示相同,不渲染;false 表示不同,渲染 |
3.7 注意事项与避坑
坑 1:memo 只进行浅比较
// ❌ 错误示例:父组件每次渲染都创建新对象
function Parent() {
const [count, setCount] = useState(0);
const user = { name: '张三' }; // 每次渲染都是新对象
return (
<div>
<button onClick={() => setCount(count + 1)}>+1</button>
<Child user={user} />
</div
);
}
const Child = memo(function Child({ user }) {
console.log('Child 渲染');
return <div>{user.name}</div>;
});
问题:即使
user.name内容没变,memo 发现引用变了,仍然会重新渲染。
正确方案:用 useMemo 稳定对象引用
import React, { useState, useMemo, memo } from "react";
function Parent() {
const [count, setCount] = useState(0);
// ✅ 用 useMemo 缓存对象,依赖项为空时引用稳定
const user = useMemo(() => ({ name: "张三" }), []);
return (
<div>
<button onClick={() => setCount(count + 1)}>+1</button>
<Child user={user} />
</div>
);
}
const Child = memo(function Child({ user }) {
console.log("Child 渲染");
return <div>{user.name}</div>;
});
export default Parent;
坑 2:memo 对函数的 props 无效(除非用 useCallback)
function Parent() {
const [count, setCount] = useState(0);
// 每次渲染都是新函数
const handleClick = () => console.log("点击");
return (
<div>
<button onClick={() => setCount(count + 1)}>+1</button>
<Child onClick={handleClick} />
</div>
);
}
const Child = memo(function Child({ onClick }) {
console.log("Child 渲染");
return <button onClick={onClick}>点击我</button>;
});
问题:
handleClick每次渲染都是新函数,memo 的浅比较会判断 props 变了,缓存失效。
正确方案:用 useCallback 稳定函数引用
import React, { useState, useCallback, memo } from "react";
function Parent() {
const [count, setCount] = useState(0);
// ✅ 用 useCallback 缓存函数
const handleClick = useCallback(() => {
console.log("点击");
}, []);
return (
<div>
<button onClick={() => setCount(count + 1)}>+1</button>
<Child onClick={handleClick} />
</div>
);
}
const Child = memo(function Child({ onClick }) {
console.log("Child 渲染");
return <button onClick={onClick}>点击我</button>;
});
export default Parent;
坑 3:memo 不是用得越多越好
类比:memo 就像给组件加了一把锁。锁能防止重复进入,但锁本身也需要检查。如果组件本身渲染很轻量(比如就显示一行文字),加 memo 反而增加比较开销,得不偿失。
建议:只对渲染较复杂、props 经常不变的组件使用 memo。
坑 4:memo 对 children 无效
如果子组件接收 children,而 children 是父组件内联创建的 JSX,那么每次渲染 children 都是新的引用,memo 会失效。
// ❌ 错误:children 每次渲染都是新对象
function Parent() {
return (
<Child>
<div>我是子内容</div>
</Child>
);
}
课堂思考题 1
为什么父组件传入一个函数 props 时,仅仅用
memo包裹子组件还不够?还需要配合哪个 Hook?
4. useMemo:值级缓存
4.1 官方定义通俗化
useMemo 的作用是:
记住一个"计算结果"。如果依赖的数据没有变化,就直接返回上一次计算好的值,避免每次渲染都重新执行昂贵的计算。
你可以把它想象成一个学生做笔记:老师讲过的题目,如果条件没变,就直接看笔记答案,不用再算一遍。
4.2 语法格式
const memoizedValue = useMemo(() => computeSomething(a, b), [a, b]);
| 部分 | 说明 |
|---|---|
| 第一个参数 | 一个返回计算结果的函数 |
| 第二个参数 | 依赖项数组,数组里的值变化时重新计算 |
| 返回值 | 计算结果 |
4.3 基础示例:缓存平方计算
import React, { useState, useMemo } from "react";
function SquareDemo() {
const [num, setNum] = useState(2);
const [count, setCount] = useState(0);
// 只有 num 变化时,才会重新计算平方
const square = useMemo(() => {
console.log("计算平方:", num);
return num * num;
}, [num]);
return (
<div>
<p>
数字:{num},平方:{square}
</p>
<button onClick={() => setNum(num + 1)}>数字 +1</button>
{/* 点击这个按钮,square 不会重新计算,因为依赖项 num 没变 */}
<button onClick={() => setCount(count + 1)}>计数 +1(不影响平方)</button>
</div>
);
}
export default SquareDemo;
4.4 场景示例一:搜索页的搜索结果计算缓存
假设有一个搜索页,需要根据关键词对大量商品进行过滤和排序。
import React, { useState, useMemo } from "react";
function SearchPage() {
const [keyword, setKeyword] = useState("");
const [sortBy, setSortBy] = useState("price"); // price 或 name
// 假设商品列表很大
const products = [
{ id: 1, name: "苹果手机", price: 5999 },
{ id: 2, name: "华为手机", price: 4999 },
{ id: 3, name: "小米手机", price: 2999 },
{ id: 4, name: "三星手机", price: 3999 },
];
// ✅ 只有 keyword 或 sortBy 变化时,才重新过滤和排序
const filteredProducts = useMemo(() => {
console.log("执行过滤和排序");
const filtered = products.filter((product) =>
product.name.toLowerCase().includes(keyword.toLowerCase()),
);
return filtered.sort((a, b) => {
if (sortBy === "price") return a.price - b.price;
return a.name.localeCompare(b.name);
});
}, [keyword, sortBy]);
return (
<div>
<input
type="text"
value={keyword}
onChange={(e) => setKeyword(e.target.value)}
placeholder="搜索商品"
/>
<select value={sortBy} onChange={(e) => setSortBy(e.target.value)}>
<option value="price">按价格</option>
<option value="name">按名称</option>
</select>
<ul>
{filteredProducts.map((product) => (
<li key={product.id}>
{product.name} - ¥{product.price}
</li>
))}
</ul>
</div>
);
}
export default SearchPage;
教学要点
useMemo的依赖项是[keyword, sortBy];- 如果只是其他无关 state(比如页面主题颜色)变化,过滤和排序不会重新执行;
- 适合计算量较大的场景。
4.5 场景示例二:稳定对象引用,配合 memo 使用
import React, { useState, useMemo, memo } from "react";
function Parent() {
const [count, setCount] = useState(0);
// ✅ 用 useMemo 稳定 user 对象,使子组件 memo 生效
const user = useMemo(
() => ({
name: "张三",
age: 18,
}),
[],
);
return (
<div>
<button onClick={() => setCount(count + 1)}>+1</button>
<UserCard user={user} />
</div>
);
}
const UserCard = memo(function UserCard({ user }) {
console.log("UserCard 渲染");
return (
<div>
{user.name} - {user.age} 岁
</div>
);
});
export default Parent;
4.6 注意事项与避坑
坑 1:useMemo 不能在条件判断中调用
// ❌ 错误:在 if 中调用 Hook
function Demo({ shouldCompute }) {
if (shouldCompute) {
const value = useMemo(() => 1 + 1, []);
}
}
原因:Hook 必须按照固定顺序调用,条件调用会破坏 React 内部的状态记录。
正确方案
// ✅ 正确:始终在组件顶层调用
function Demo({ shouldCompute }) {
const value = useMemo(() => {
if (!shouldCompute) return 0;
return 1 + 1;
}, [shouldCompute]);
return <div>{value}</div>;
}
坑 2:依赖项遗漏导致拿到旧值
// ❌ 错误:依赖项没包含 num
function WrongDeps() {
const [num, setNum] = useState(1);
const double = useMemo(() => {
console.log("计算 double");
return num * 2;
}, []); // 漏了 num!
return (
<div>
<p>{double}</p>
<button onClick={() => setNum(num + 1)}>+1</button>
</div>
);
}
问题:点击 +1 后,
double不会更新,因为useMemo认为没有依赖变化。
正确方案
// ✅ 正确:依赖项包含 num
const double = useMemo(() => num * 2, [num]);
坑 3:简单计算没必要用 useMemo
// ❌ 没必要:1 + 1 的计算开销极小
const sum = useMemo(() => 1 + 1, []);
类比:你不需要为了记住"2 + 2 = 4"专门写个笔记本。复杂计算才值得缓存。
坑 4:useMemo 返回对象/数组时,引用会变化吗?
如果依赖项没变,useMemo 返回的是同一个对象/数组引用。这正好配合 memo 使用。
但如果依赖项变了,即使内容相同,也是新的引用。
课堂思考题 2
如果一个
useMemo的依赖项数组写成空数组[],它的返回值会变化吗?在什么情况下会变化?
5. useCallback:函数级缓存
5.1 官方定义通俗化
useCallback 的作用是:
记住一个函数。如果依赖项没有变化,就返回上一次创建的同一个函数,而不是每次渲染都创建新函数。
你可以把它理解成给函数发了一张身份证:只要身份证上的信息(依赖项)没变,就认为是同一个人(同一个函数)。
5.2 语法格式
const memoizedCallback = useCallback(() => {
doSomething(a, b);
}, [a, b]);
| 部分 | 说明 |
|---|---|
| 第一个参数 | 要缓存的函数 |
| 第二个参数 | 依赖项数组,变化时重新创建函数 |
| 返回值 | 缓存后的函数 |
5.3 基础示例:稳定事件处理函数
import React, { useState, useCallback, memo } from "react";
function Parent() {
const [count, setCount] = useState(0);
// ✅ 用 useCallback 缓存函数,依赖项为空时函数引用不变
const handleClick = useCallback(() => {
console.log("按钮被点击");
}, []);
return (
<div>
<p>计数:{count}</p>
<button onClick={() => setCount(count + 1)}>父组件 +1</button>
<ChildButton onClick={handleClick} />
</div>
);
}
const ChildButton = memo(function ChildButton({ onClick }) {
console.log("ChildButton 渲染");
return <button onClick={onClick}>子组件按钮</button>;
});
export default Parent;
运行效果
- 没加
useCallback:父组件每次更新都会创建新函数,子组件memo失效,不断渲染。 - 加上
useCallback:函数引用稳定,子组件不渲染。
5.4 场景示例一:表单页的提交函数缓存
import React, { useState, useCallback, memo } from "react";
// 表单提交按钮子组件
const SubmitButton = memo(function SubmitButton({ onSubmit }) {
console.log("SubmitButton 渲染");
return <button onClick={onSubmit}>提交</button>;
});
function FormPage() {
const [name, setName] = useState("");
const [age, setAge] = useState("");
// ✅ 只有当 name 或 age 变化时,才重新创建提交函数
const handleSubmit = useCallback(() => {
console.log("提交数据:", { name, age });
alert(`提交成功:${name},${age} 岁`);
}, [name, age]);
return (
<div>
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="姓名"
/>
<input
type="text"
value={age}
onChange={(e) => setAge(e.target.value)}
placeholder="年龄"
/>
<SubmitButton onSubmit={handleSubmit} />
</div>
);
}
export default FormPage;
教学要点
- 表单输入框是频繁变化的状态;
- 提交函数依赖
name和age,所以依赖项要写[name, age]; - 如果按钮组件被
memo包裹,只有当name或age变化时按钮才会重新渲染。
5.5 场景示例二:依赖 props 的回调函数
import React, { useState, useCallback, memo } from "react";
const Child = memo(function Child({ onSelect, id }) {
console.log("Child 渲染:", id);
return <button onClick={() => onSelect(id)}>选择 {id}</button>;
});
function Parent() {
const [selectedId, setSelectedId] = useState(null);
// ✅ 只有当 setSelectedId 变化时才重新创建(它其实不会变)
const handleSelect = useCallback((id) => {
setSelectedId(id);
}, []);
return (
<div>
<p>已选择:{selectedId ?? "无"}</p>
{[1, 2, 3].map((id) => (
<Child key={id} id={id} onSelect={handleSelect} />
))}
</div>
);
}
export default Parent;
教学要点
handleSelect内部使用了setSelectedId,但setState函数在 React 中是稳定的,不需要放入依赖项;- 使用
useCallback后,即使父组件的selectedId变化,子组件的onSelect引用仍然不变,子组件不会重新渲染。
5.6 注意事项与避坑
坑 1:useCallback 过度使用反而占用内存
// ❌ 错误:所有函数都包 useCallback
function Demo() {
const handleClick1 = useCallback(() => {}, []);
const handleClick2 = useCallback(() => {}, []);
// ... 很多个
}
类比:缓存就像把东西存进仓库。东西太多,仓库管理成本也会变高。对于简单、不传子组件的函数,不需要 useCallback。
正确方案
只对以下情况使用 useCallback:
- 函数作为 props 传给被
memo包裹的子组件; - 函数被放入
useEffect、useMemo的依赖项数组中; - 函数被多个地方复用,需要稳定引用。
坑 2:依赖项遗漏导致闭包陷阱
// ❌ 错误:count 没在依赖项中
function Counter() {
const [count, setCount] = useState(0);
const handleAlert = useCallback(() => {
alert(`当前计数:${count}`);
}, []); // 漏了 count
return <button onClick={handleAlert}>显示计数</button>;
}
问题:点击按钮时,永远显示
当前计数:0,因为函数一直记住的是最初的 count。
正确方案
// ✅ 正确:依赖项包含 count
const handleAlert = useCallback(() => {
alert(`当前计数:${count}`);
}, [count]);
坑 3:useCallback 不等于"函数性能更好"
有些同学以为加了
useCallback函数执行就更快了。其实不是的。useCallback只是让函数引用不变,函数体该执行的时候还是会执行。它解决的是"子组件不必要的渲染"和"依赖项稳定"问题,而不是函数本身的执行速度。
坑 4:useCallback 内部用到的所有变量都要放入依赖项
// ✅ 推荐写法:eslint 自动检查依赖项
const handleSubmit = useCallback(() => {
submitForm(name, age);
}, [name, age]);
React 官方推荐开启 react-hooks/exhaustive-deps ESLint 规则,自动提示遗漏的依赖项。
课堂思考题 3
为什么不加
useCallback,子组件每次拿到的提交函数都是新的?这会导致什么问题?
6. 三兄弟对比总结
| 特性 | React.memo |
useMemo |
useCallback |
|---|---|---|---|
| 作用级别 | 组件级 | 值级 | 函数级 |
| 缓存对象 | 整个组件渲染结果 | 任意计算值 | 函数 |
| 使用位置 | 包裹组件导出 | 组件内部 | 组件内部 |
| 触发条件 | props 浅比较相同 | 依赖项相同 | 依赖项相同 |
| 典型场景 | 子组件 props 不变时跳过渲染 | 复杂计算、稳定对象引用 | 稳定事件处理函数、传给 memo 子组件的回调 |
| 常见搭配 | useCallback + useMemo |
React.memo |
React.memo |
| 是否适用类组件 | ✅ 是 | ❌ 否(函数组件专用) | ❌ 否(函数组件专用) |
使用决策流程图
是否有子组件因为父组件更新而重复渲染?
↓
是 → 子组件用 React.memo 包裹
↓
子组件是否接收函数 props?
↓
是 → 父组件用 useCallback 稳定函数
↓
子组件是否接收对象/数组 props?
↓
是 → 父组件用 useMemo 稳定引用
↓
渲染中是否有复杂计算?
↓
是 → 用 useMemo 缓存计算结果
7. 综合实战:商品列表页优化
下面把三个 API 组合起来,完成一个完整的商品列表页优化案例。
import React, { useState, useMemo, useCallback, memo } from "react";
// 商品卡片:memo 组件级缓存
const ProductCard = memo(function ProductCard({ product, onAdd }) {
console.log("渲染商品卡片:", product.name);
return (
<div className="product-card">
<h3>{product.name}</h3>
<p>价格:¥{product.price}</p>
<button onClick={() => onAdd(product.id)}>加入购物车</button>
</div>
);
});
function ProductListPage() {
const [keyword, setKeyword] = useState("");
const [category, setCategory] = useState("all");
const [cartCount, setCartCount] = useState(0);
// 模拟大量商品数据
const products = useMemo(
() => [
{ id: 1, name: "苹果手机", price: 5999, category: "phone" },
{ id: 2, name: "华为手机", price: 4999, category: "phone" },
{ id: 3, name: "小米笔记本", price: 4999, category: "computer" },
{ id: 4, name: "MacBook", price: 9999, category: "computer" },
{ id: 5, name: "AirPods", price: 1299, category: "audio" },
],
[],
);
// ✅ useMemo 缓存过滤结果,只有 keyword 或 category 变化才重新计算
const filteredProducts = useMemo(() => {
console.log("执行商品过滤");
return products
.filter((product) =>
product.name.toLowerCase().includes(keyword.toLowerCase()),
)
.filter((product) =>
category === "all" ? true : product.category === category,
);
}, [keyword, category, products]);
// ✅ useCallback 缓存加入购物车函数,依赖 cartCount 时会重新创建
const handleAddToCart = useCallback((productId) => {
console.log("加入购物车:", productId);
setCartCount((count) => count + 1);
}, []);
return (
<div>
<h1>商品列表(购物车:{cartCount} 件)</h1>
<input
type="text"
value={keyword}
onChange={(e) => setKeyword(e.target.value)}
placeholder="搜索商品"
/>
<select value={category} onChange={(e) => setCategory(e.target.value)}>
<option value="all">全部</option>
<option value="phone">手机</option>
<option value="computer">电脑</option>
<option value="audio">音频</option>
</select>
<div className="product-list">
{filteredProducts.map((product) => (
<ProductCard
key={product.id}
product={product}
onAdd={handleAddToCart}
/>
))}
</div>
</div>
);
}
export default ProductListPage;
优化点分析
| 优化点 | 使用的 API | 效果 |
|---|---|---|
| 商品卡片避免重复渲染 | React.memo |
商品数据不变时,卡片不重新渲染 |
| 搜索过滤结果避免重复计算 | useMemo |
无关 state 变化时,不重新过滤 |
| 加入购物车函数引用稳定 | useCallback |
memo 子组件不会因为函数引用变化而失效 |
8. 高频问题与学生提问预判
Q1:memo、useMemo、useCallback 会让我的组件变快吗?
答:不一定。它们解决的是"不必要的渲染和计算"。如果组件本身渲染很简单,加缓存反而有比较成本。适合在以下场景使用:
- 子组件渲染复杂但 props 经常不变;
- 组件内部有复杂计算;
- 函数作为 props 传给 memo 子组件。
Q2:为什么 memo 子组件收到函数 props 时还会重新渲染?
答:因为父组件每次渲染都会创建一个新的函数对象。即使函数体内容一样,引用不同,memo 的浅比较会认为 props 变了。需要用 useCallback 稳定函数引用。
Q3:useMemo 和 useCallback 有什么区别?
答:它们本质很像,都是依赖缓存。区别是:
useMemo缓存的是值(计算结果);useCallback缓存的是函数。
实际上,useCallback(fn, deps) 等价于 useMemo(() => fn, deps)。
Q4:依赖项数组为空 [] 是什么意思?
答:表示只在组件挂载时创建一次,之后永远复用。适合那些不依赖任何 state 或 props 的值/函数。
但要注意:如果函数内部用到了 state 或 props,却漏写依赖项,就会形成闭包陷阱,拿到旧值。
Q5:什么时候不需要用缓存?
答:
- 组件渲染很轻量;
- 函数不传子组件;
- 计算非常简单(比如
a + b); - 对象是临时创建且不会传给 memo 子组件。
滥用缓存会让代码变复杂,甚至内存占用增加。
Q6:浅比较是什么意思?
答:浅比较只比较对象或数组的引用是否相同,不会深入比较里面的内容。例如:
const a = { name: "张三" };
const b = { name: "张三" };
// 浅比较会认为 a 和 b 不同,因为引用不同
console.log(a === b); // false
所以 memo 看到父组件传入的新对象,即使内容一样,也会重新渲染。
Q7:React.memo 和 PureComponent 有什么区别?
答:React.memo 是给函数组件用的,PureComponent 是给类组件用的。它们的原理类似:都是对新旧 props 进行浅比较,如果相同则跳过渲染。
课堂小结
| API | 一句话记住 | 什么时候用 |
|---|---|---|
React.memo |
给组件穿记忆外套 | 子组件渲染复杂、props 不变 |
useMemo |
给计算结果做笔记 | 复杂计算、稳定对象引用 |
useCallback |
给函数发身份证 | 函数传给 memo 子组件、函数放入依赖项 |
💡 核心原则:缓存是优化手段,不是装饰手段。先写清楚代码,遇到性能问题再按需加缓存,优先使用 React Profiler 分析后再决定。
附录:常见错误速查表
| 错误 | 现象 | 修正方案 |
|---|---|---|
| memo 子组件仍重复渲染 | 父组件传入对象/函数新引用 | 用 useMemo/useCallback 稳定引用 |
| useMemo 不更新 | 依赖项遗漏 | 补全依赖项 |
| useCallback 闭包旧值 | 依赖项遗漏 | 补全依赖项 |
| 滥用缓存导致代码复杂 | 简单组件/函数也包缓存 | 只对必要场景使用 |
| 在条件中调用 useMemo/useCallback | 程序报错 | 始终在组件顶层调用 |