❗ 点击计数按钮,为什么"峨眉队"也跟着重新渲染?React.memo 说:我记住了

写在前面:今天学了一个让我"恍然大悟"的知识------React 的性能优化 Hooks:memouseCallbackuseMemo 。老师先抛出一个问题:"父组件有多个状态,子组件只依赖其中的一个。父组件重新渲染时,子组件也会跟着重新渲染------这有必要吗?" 我们做了个实验:点击"计数"按钮,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 记住结果。"

然后看着面试官满意的表情,心里默念:这波,又稳了。


本文所有代码示例均来自课堂学习资料,真实可运行。

相关推荐
31535669132 小时前
DeepSeek Harness 发布后,我没急着跑 Demo,先把 `.agents/` 翻了一遍
前端·后端·github
名字还没想好☜2 小时前
Next.js 用 Server Components 直连数据库:去掉 API 层的边界,和三条别踩的安全红线
前端·javascript·数据库·安全·react·next.js
学习zhao极致it2 小时前
2020全新React教程全家桶实战redux+antd+React Hooks前端js视频
前端
用户921080262862 小时前
Bubble 消息操作区改造:复制、重新生成和反馈
前端
LEE2 小时前
AI Agent 都在疯狂加功能,它说:我全砍了
前端·后端
张清悠2 小时前
用 TRAE Work 把 PRD + 设计稿注释,整理成 20 分钟可排期前端任务清单
前端
Brown.alexis3 小时前
es6知识点5-自备使用
前端·javascript·es6
杨利杰YJlio3 小时前
KB5121003更新详解:安全启动证书、AI组件、资源管理器与安装验证
前端·javascript·后端
紫菜猫一只3 小时前
Vue 3 与 React 常用写法对比
前端