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

12 阅读6分钟

写在前面:今天学了一个让我"恍然大悟"的知识——React 的性能优化 Hooks:memo、useCallback、useMemo。老师先抛出一个问题:"父组件有多个状态,子组件只依赖其中的一个。父组件重新渲染时,子组件也会跟着重新渲染——这有必要吗?" 我们做了个实验:点击"计数"按钮,count 变了,但 name 根本没变,然而普通子组件照样重新渲染。老师说:"这就是性能浪费。memo 就是为了解决这个——'请记住我,我的 props 没变就别重渲染我'。"


一、问题现场:父组件一渲染,子组件全遭殃

1.1 实验代码

看 App.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 没变 → 跳过渲染,复用上一次的结果。
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 里有函数呢?

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 的解决方案

import { useCallback } from 'react';

function App() {
    // 依赖数组 []:只有组件卸载前,这个函数引用一直不变
    const handleClick = useCallback(() => {
        console.log('点击了');
    }, []);

    return <MemoChild onClick={handleClick} />;
}

useCallback(fn, deps) 的作用:

  • 第一次渲染:创建函数,并"记住"它。
  • 后续渲染:如果依赖数组 deps 没变,返回上一次记住的那个函数(引用不变)。

这样传给 memo 子组件的函数引用永远稳定——memo 就能真正发挥作用了。


四、useMemo:让"计算结果"缓存起来

4.1 问题:昂贵的计算每次渲染都执行

function App() {
    const [count, setCount] = useState(0);

    // 昂贵的计算:每次渲染都执行一次!
    const expensiveResult = heavyComputation(count);

    return <div>{expensiveResult}</div>;
}

heavyComputation 很耗时,但 count 没变的时候,它的结果其实没变——每次渲染都重算,纯浪费。

4.2 useMemo 的解决方案

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计算结果昂贵计算避免重复执行值

记忆口诀:

memo:记住"组件"——我 props 没变,别重渲染我
useCallback:记住"函数"——我引用不变,memo 兄弟靠得住
useMemo:记住"结果"——我依赖没变,别重算我

三者配合使用的经典场景:

// 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 记住结果。"

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


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