Kodokon kodokon.com

记忆化:memo、useMemo、useCallback

搞清楚 React.memo、useMemo 和 useCallback 什么时候真正让你的应用变快,什么时候只是白白让它变复杂。

10 分钟 · 3 题

在 Kodokon 中打开本课

当一个组件重新渲染时,它的所有子组件默认都会重新渲染,无论它们的 prop 有没有变化。这并不是灾难:一次渲染只有在输出改变时才会触碰 DOM。当频繁的渲染碰上开销大的子树时,记忆化才变得有意义。三个工具,三个目标:React.memo 在组件的 prop 浅相等时跳过其渲染,useMemo 缓存一个计算出来的useCallback 缓存一个函数。三者都依赖引用相等。

JSX
import { memo } from "react";

const Row = memo(function Row({ item, onSelect }) {
  return (
    <li onClick={() => onSelect(item.id)}>{item.label}</li>
  );
});
只有当 item 或 onSelect 的引用改变时,Row 才会重新渲染。

链条的强度取决于它最薄弱的一环。如果哪怕只有一个 prop 在每次渲染时收到新引用——一个内联箭头函数、一个对象或数组字面量——memo 的比较就会失败,组件照样重新渲染。useCallback 稳定函数的引用,从而让比较得以成功。

JSX
import { useCallback } from "react";

function List({ items }) {
  const handleSelect = useCallback((id) => {
    console.log("selected:", id);
  }, []);

  return (
    <ul>
      {items.map((item) => (
        <Row
          key={item.id}
          item={item}
          onSelect={handleSelect}
        />
      ))}
    </ul>
  );
}
handleSelect 在每次渲染时都保持相同的引用。

useMemo 有两种正当用途:避免一次真正开销大的计算(对成千上万条数据排序或聚合),以及稳定一个引用,供 memo 化的子组件消费或列在某个 effect 的依赖里。对于一个微不足道的计算,缓存的成本超过它节省的:依赖比较、占用内存、读起来更费劲。

JSX
import { useMemo } from "react";

function Stats({ orders }) {
  const total = useMemo(() => {
    return orders.reduce(
      (sum, order) => sum + order.amount,
      0
    );
  }, [orders]);

  return <p>Total: {total} €</p>;
}
只有当 orders 的引用改变时,total 才会被重新计算。

知识检测

确认你已牢记本课的重点内容。

  1. 一个子组件被包在 React.memo 里,但它的父组件把一个内联箭头函数作为 prop 传给它。结果如何?
    • 记忆化生效:函数是按其源代码来比较的
    • 记忆化无效:内联函数在每次渲染时都创建一个新引用,比较随之失败
    • React 在开发环境中会抛出一条警告
  2. 只要 a 不变,useCallback(fn, [a]) 保证的是什么?
    • 函数在内存中永远不会被重建
    • 函数只会被执行一次
    • 从一次渲染到下一次,返回的是相同的函数引用
  3. 在哪种情况下 useMemo 明显是合理的?
    • 一个开销大的计算,或者一个要为 memo 化子组件或 effect 依赖而稳定的引用
    • 任何派生值,出于预防都要用
    • 在组件的两次挂载之间缓存数据