目 录CONTENT

文章目录

React(十五)缓存

React(十五)缓存完全指南:memo、useMemo、useCallback

目标:彻底搞懂 React 19 中三个缓存 API 的作用、用法和避坑点。
本文面向课堂教学,从 React 渲染机制出发,结合生活案例和完整代码示例,帮助你判断"什么时候该用缓存"。


目录

  1. 为什么需要缓存?先理解 React 的渲染机制
  2. 缓存三兄弟速览
  3. React.memo:组件级缓存
  4. useMemo:值级缓存
  5. useCallback:函数级缓存
  6. 三兄弟对比总结
  7. 综合实战:商品列表页优化
  8. 高频问题与学生提问预判

1. 为什么需要缓存?先理解 React 的渲染机制

1.1 父组件更新,子组件默认也会重新渲染

在 React 中,组件通过 props 接收数据。当父组件的 stateprops 发生变化时,父组件会重新渲染。而默认情况下,父组件重新渲染后,它内部所有的子组件也会跟着重新渲染。

听起来有点浪费?确实是这样。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 重新渲染了 被打印很多次。可是 Childtitle 根本没变,它却在反复执行渲染。

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 数组没变,每个 ProductCardproduct 引用也没变;
  • 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;

教学要点

  • 表单输入框是频繁变化的状态;
  • 提交函数依赖 nameage,所以依赖项要写 [name, age]
  • 如果按钮组件被 memo 包裹,只有当 nameage 变化时按钮才会重新渲染。

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 包裹的子组件;
  • 函数被放入 useEffectuseMemo 的依赖项数组中;
  • 函数被多个地方复用,需要稳定引用。

坑 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 程序报错 始终在组件顶层调用

0
博主关闭了当前页面的评论