写在前面:今天学了一个让我"恍然大悟"的知识------React 的性能优化 Hooks:
memo、useCallback、useMemo。老师先抛出一个问题:"父组件有多个状态,子组件只依赖其中的一个。父组件重新渲染时,子组件也会跟着重新渲染------这有必要吗?" 我们做了个实验:点击"计数"按钮,count变了,但name根本没变,然而普通子组件照样重新渲染。老师说:"这就是性能浪费。memo就是为了解决这个------'请记住我,我的 props 没变就别重渲染我'。"
一、问题现场:父组件一渲染,子组件全遭殃
1.1 实验代码
看 App.jsx:
jsx
import { useState, memo } from 'react';
// 普通子组件:父组件每次渲染它都跟着渲染
function RegularChild({ name }) {
console.log('渲染了 RegularChild');
return <h1>{name}</h1>;
}
// 用 memo 包裹的子组件:props 没变就不重新渲染
const MemoChild = memo(({ name }) => {
console.log('MemoizedChild 渲染了');
return <div>Hello, {name}</div>;
});
function App() {
const [count, setCount] = useState(0);
const [name, setName] = useState('少林队');
return (
<>
<button onClick={() => setCount(count + 1)}>点击计数{count}</button>
<button onClick={() => setName("峨眉队")}>改变名字</button>
{/* 两个子组件都只依赖 name */}
<RegularChild name={name} />
<MemoChild name={name} />
</>
);
}
1.2 现场实验:点击"计数"按钮
操作:点一下"点击计数"按钮(count 从 0 → 1)。
控制台输出:
App 组件渲染
渲染了 RegularChild ← 普通子组件:又渲染了!
注意:MemoizedChild 渲染了 没有打印!
结论:
- RegularChild(普通组件) :
count变了,name没变,但它依然重新渲染了------浪费! - MemoChild(memo 组件) :
name没变,它拒绝了重新渲染------性能优化!
老师说:
"父组件有多个状态,子组件依赖其中的一些状态。父组件重新渲染,子组件也会重新渲染,带来性能的浪费。希望不相关的属性发生改变时,拒绝重新渲染。
memo(memorize)------请记住我。"
二、memo:React 的"记忆大师"
2.1 memo 是什么?
memo 是 React 提供的高阶组件(HOC),它"记住了"上一次的 props。 当父组件重新渲染时,memo 会对比 props 是否变化:
- props 变了 → 重新渲染。
- props 没变 → 跳过渲染,复用上一次的结果。
jsx
const MemoChild = memo(({ name }) => {
return <div>Hello, {name}</div>;
});
memo(组件) 把普通组件"升级"成会记忆的组件。
2.2 工作原理:浅比较(Shallow Compare)
memo 默认做"浅比较"------只对比 props 的引用是否变化。
父组件重新渲染
↓
memo 对比 props(浅比较)
├── props 引用变了 → 重新渲染
└── props 引用没变 → 跳过渲染(性能提升!)
注意:浅比较只对比第一层。 如果传的是对象,对象内容变了但引用没变,memo 检测不到------这是 memo 的"盲区"。
2.3 什么时候该用 memo?
| 场景 | 用不用 memo |
|---|---|
| 子组件渲染很重(大量 DOM、复杂计算) | ✅ 用 |
| 子组件频繁被父组件无关状态触发渲染 | ✅ 用 |
| 子组件很轻量(渲染成本低) | ❌ 不用(memo 也有对比成本) |
| 子组件依赖的 props 经常变 | ❌ 不用(每次都变,白对比) |
memo 不是"用了就一定快"------它是有成本的(每次对比 props)。要"重子组件 + props 稳定"才值得用。
三、useCallback:让函数"稳定"下来
3.1 问题:函数每次都是新的
memo 只对比 props 的引用。但如果传给子组件的 props 里有函数呢?
jsx
const MemoChild = memo(({ onClick }) => {
return <button onClick={onClick}>点我</button>;
});
function App() {
const handleClick = () => {
console.log('点击了');
};
// 每次渲染 handleClick 都是一个新的函数引用!
return <MemoChild onClick={handleClick} />;
}
问题:handleClick 在每次渲染时都是"新的函数"------引用变了!
- 父组件渲染 → 创建新的
handleClick→ props 引用变了 → memo 检测到变化 → 子组件照样重新渲染。
memo 白干了!
3.2 useCallback 的解决方案
jsx
import { useCallback } from 'react';
function App() {
// 依赖数组 []:只有组件卸载前,这个函数引用一直不变
const handleClick = useCallback(() => {
console.log('点击了');
}, []);
return <MemoChild onClick={handleClick} />;
}
useCallback(fn, deps) 的作用:
- 第一次渲染:创建函数,并"记住"它。
- 后续渲染 :如果依赖数组
deps没变,返回上一次记住的那个函数(引用不变)。
这样传给 memo 子组件的函数引用永远稳定------memo 就能真正发挥作用了。
四、useMemo:让"计算结果"缓存起来
4.1 问题:昂贵的计算每次渲染都执行
jsx
function App() {
const [count, setCount] = useState(0);
// 昂贵的计算:每次渲染都执行一次!
const expensiveResult = heavyComputation(count);
return <div>{expensiveResult}</div>;
}
heavyComputation 很耗时,但 count 没变的时候,它的结果其实没变------每次渲染都重算,纯浪费。
4.2 useMemo 的解决方案
jsx
import { useMemo } from 'react';
function App() {
const [count, setCount] = useState(0);
// 只有 count 变化时才重新计算
const expensiveResult = useMemo(
() => heavyComputation(count),
[count]
);
return <div>{expensiveResult}</div>;
}
useMemo(() => 计算, [依赖]) 的作用:
- 依赖没变 → 直接返回上一次缓存的结果(不重新计算)。
- 依赖变了 → 重新计算,并更新缓存。
五、三兄弟的区别:一张表看懂
| Hook | 记住什么 | 解决什么问题 | 返回什么 |
|---|---|---|---|
| memo | 组件本身 | 子组件 props 没变时拒绝重渲染 | 新组件 |
| useCallback | 函数 | 函数引用稳定,配合 memo 生效 | 函数 |
| useMemo | 计算结果 | 昂贵计算避免重复执行 | 值 |
记忆口诀:
arduino
memo:记住"组件"------我 props 没变,别重渲染我
useCallback:记住"函数"------我引用不变,memo 兄弟靠得住
useMemo:记住"结果"------我依赖没变,别重算我
三者配合使用的经典场景:
jsx
// 1. useMemo 缓存计算结果
const filteredList = useMemo(
() => items.filter(item => item.active),
[items]
);
// 2. useCallback 缓存函数
const handleToggle = useCallback((id) => {
setItems(prev => prev.map(...));
}, []);
// 3. memo 包裹子组件,props 稳定时拒绝重渲染
const MemoList = memo(List);
<MemoList items={filteredList} onToggle={handleToggle} />
六、总结:React 性能优化三件套
| 概念 | 说明 |
|---|---|
| memo | 高阶组件,props 没变时跳过子组件渲染 |
| 浅比较 | memo 默认只对比 props 第一层引用 |
| useCallback | 缓存函数引用,配合 memo 使用 |
| useMemo | 缓存计算结果,依赖没变不重算 |
| 依赖数组 | [] 空 = 永远稳定;有值 = 值变才更新 |
| 适用条件 | 重子组件 + props 稳定才值得用 |
性能优化的核心思想:能不做的事就别做。memo 让不相关状态的子组件不重渲染,useCallback 让函数引用稳定,useMemo 让昂贵计算不重复------三个 Hook 组合起来,React 应用才能又流畅又高效。
写在最后
今天最大的收获,是亲眼看到了"父组件渲染导致子组件无辜重渲染"的性能浪费。点击计数按钮,name 一点没变,普通子组件却老老实实重新渲染了一遍------就像你给张三发消息,李四的手机也响了一样莫名其妙。
下次面试官问你:"memo、useCallback、useMemo 有什么区别?"
你可以淡定地说:
"三者都是 React 的性能优化 Hooks。memo 是高阶组件,包裹子组件后,当 props 浅比较没有变化时,父组件重新渲染也会跳过子组件------适合渲染成本高、props 稳定的子组件。useCallback 缓存函数引用,因为如果 props 里传函数,每次渲染函数引用都会变,memo 就失效了;useCallback(fn, deps) 在 deps 不变时返回同一个函数引用。useMemo 缓存计算结果,useMemo(() => calc, deps) 在 deps 不变时直接返回缓存值,避免昂贵计算重复执行。记忆口诀:memo 记住组件、useCallback 记住函数、useMemo 记住结果。"
然后看着面试官满意的表情,心里默念:这波,又稳了。
本文所有代码示例均来自课堂学习资料,真实可运行。