找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

1119

积分

0

好友

141

主题
发表于 12 小时前 | 查看: 0| 回复: 0

原文地址:https://hiimvikash.medium.com/4-usememo-usecallback-react-memo-when-it-works-when-it-breaks-and-why-8b9fe4fae536

原文作者: Vikash Gupta

我想聊一个几乎总是和“优化 React”绑定在一起的东西——但老实说,它至少有一半时间并没有按照我们想象的方式工作:memoization。没错,就是那几个老熟人:useMemo、useCallback 和 React.memo

而且我一点也没有夸张地说“有一半时间”。正确地使用 memoization 其实非常棘手——比表面看起来要难得多。等你读完这篇文章,大概率会一边点头一边感叹“确实如此”。我们将会拆解以下内容:

  • memoization 真正要解决的问题是什么(提示:不是单纯的性能)
  • useMemo 和 useCallback 在底层到底做了什么,以及它们之间的区别
  • 为什么盲目地 memoize props 实际上是一种反模式
  • React.memo 真正的作用,以及你必须遵守的使用规则
  • memoization 如何与 “elements as children” 模式相互影响
  • useMemo 在什么情况下才真的能帮到昂贵计算
  • 真正的核心问题:值的比较

真正的问题:值是如何被比较的

一切都要从 JavaScript 如何比较值开始。原始类型——数字、字符串、布尔值——是按值比较的:

const a = 1;
const b = 1;
a === b; // true

很简单,对吧?

对象(包括数组和函数)就完全不同了。它们是按引用比较的,而不是按内容。当你这样写时:

const a = { id: 1 };
const b = { id: 1 };

即使这两个对象看起来一模一样,它们在内存中是两个不同的引用。因此:

a === b; // false

只有当两个变量指向完全相同的引用时,比较才会为 true:

const a = { id: 1 };
const b = a;
a === b; // true

而这,正是 React 开始“头疼”的地方。


重新渲染 = 新的引用

每当组件 re-render,React 都会重新执行组件函数。这意味着组件内部的所有局部变量——包括函数——都会被重新创建。

一次新的 render = 一个新的函数引用。

当 React 在 hooks(比如 useEffect)中检查 dependency 时,它做的是引用比较

const Component = () => {
  const submit = () => {};

  useEffect(() => {
    submit();
  }, [submit]); // submit 每次 render 都是新引用
};

因为 submit 每次都会被重新创建,React 总是认为它“变了”,于是 useEffect 在每一次 render时都会执行——即使代码本身完全一样。

useMemo 和 useCallback:它们真正的工作方式

既然我们理解了引用变化的问题,那接下来就该聊聊如何解决它了。我们需要一种方式,让某个值在 re-render 之间保持同一个引用,这样 React 才不会误以为它发生了变化。

这正是 useMemouseCallback 的用武之地。

这两个 Hook 看起来很像,本质目标也完全一致:

只要依赖没有变化,就保持引用不变。

例如,用 useCallback 包裹 submit:

const submit = useCallback(() => {
  // no dependencies, reference won‘t change
}, []);

现在,submit 在多次 re-render 之间会保持相同的引用。React 的比较结果为 true,依赖它的 useEffect 也就不会在每次 render 时都触发了:

const Component = () => {
  const submit = useCallback(() => {
    // submit something here
  }, []);

  useEffect(() => {
    submit();
    // submit 是 memoized 的,不会在每次 render 触发
  }, [submit]);

  return ...;
};

useMemo 也是同样的思路。区别在于:useMemo memoize 的是函数的返回值

const submit = useMemo(() => {
  return () => {
    // submit 函数是 useMemo 返回的结果
  };
}, []);

真实实现当然要复杂一些,但核心思想就是这样。

一个很小但很重要的区别

它们的 API 有一个关键差异:

  • useCallback → 你直接传入要 memoize 的函数
  • useMemo → 你传入一个函数,memoize 的是这个函数的返回值

因为这两个 Hook 的第一个参数都是函数,并且它们是在组件内部调用的,所以有一点非常重要:

你传入的那个函数本身,每次 render 都会被重新创建。 这是 JavaScript 的行为,不是 React 的问题。

例如:

const func = (callback) => {
  // do something
};

func(() => {}); // 第一次调用
func(() => {}); // 第二次调用 —— 新函数

useMemo 和 useCallback 也是一样的。它们只是普通函数,只不过 React 会把它们和组件生命周期绑定起来。

React 在底层到底做了什么

React 本质上是在缓存结果。可以用一个极简的伪实现来理解:

useCallback 的伪实现:

let cachedCallback;

const func = (callback) => {
  if (dependenciesEqual()) {
    return cachedCallback;
  }
  cachedCallback = callback;
  return callback;
};
  • 依赖没变 → 返回缓存的函数
  • 依赖变了 → 更新缓存

useMemo 的伪实现:

let cachedResult;

const func = (callback) => {
  if (dependenciesEqual()) {
    return cachedResult;
  }
  cachedResult = callback();
  return cachedResult;
};

思路完全一致,只是 useMemo 缓存的是 callback 的返回值,而不是 callback 本身。

反模式:为了 memoize 而 memoize props

这是最常见、也最容易犯的错误之一:“顺手”给 props 加 useCallback / useMemo。

你大概率见过(或者写过)这样的代码:

const Component = () => {
  const onClick = useCallback(() => {
    // do something on click
  }, []);

  return <button onClick={onClick}>click me</button>;
};

看起来很干净,对吧?但现实是——这个 useCallback 什么也没优化

有一种非常流行的误解(甚至 ChatGPT 以前也这么说过):memoize props 可以防止组件 re-render。

这是错的。

如果组件 re-render,那么组件内部的一切都会重新执行。给 onClick 包一层 useCallback 并不会改变这一点。

在这个例子中:

  • 没有性能收益
  • React 反而做了更多工作
  • 代码可读性还变差了

而且现实中,useCallback 从来不会只出现一个。一个接一个,依赖相互交织,最后代码变成一片 useMemo / useCallback 的迷宫,没人敢动。

那什么时候才真的需要 memoize props?

实际上,只有 两种 情况。


1. 当这个 prop 会被 child 用作依赖

例如:

const Parent = () => {
  // 必须 memoize
  // Child 在 useEffect 中使用它
  const fetch = () => {};

  return <Child onMount={fetch} />;
};

const Child = ({ onMount }) => {
  useEffect(() => {
    onMount();
  }, [onMount]);
};

很直接:

如果一个 prop 会进入 dependency array,它必须是稳定引用。

否则 hook 每次 render 都会执行。这条规则适用于整条 props 传递链。


2. 当组件被 React.memo 包裹时

React.memo 用来 memoize 组件本身。它告诉 React:

“如果 parent re-render,但 props 没变,就不要 re-render 我。”

就是这么简单。

但前提是:props 必须是稳定的。

下面是一个经典“看起来对,其实错”的例子:

const Child = ({ data, onChange }) => {};
const ChildMemo = React.memo(Child);

const Component = () => {
  return (
    <ChildMemo
      data={{ ...some_object }}
      onChange={() => { /* ... */ }}
    />
  );
};

data 和 onChange 都是 inline 创建的,每次 render 都是新引用。

结果就是:

  • React 检查 props
  • 发现变了
  • ChildMemo 依然 re-render

React.memo 直接失效。

useMemo / useCallback 真正该出场的地方

const Child = ({ data, onChange }) => {};
const ChildMemo = React.memo(Child);

const Component = () => {
  const data = useMemo(() => ({ ... }), []);
  const onChange = useCallback(() => {}, []);

  return <ChildMemo data={data} onChange={onChange} />;
};

现在 data 和 onChange 在 render 之间保持同一个引用。React 对比 props,发现没变,于是跳过 re-render。

👉 这才是 memoization 正确且有意义的用法。

但问题在于……

确保传入 memoized component 的所有 props 都是稳定的,远比想象中困难。

只要有一个 prop 不稳定,整条链就会崩塌:

  • React.memo 失效
  • useCallback 变得毫无意义
  • useMemo 也白写了

React.memo 和“从 props 传下来的 props”

这是 memoization 最容易被无意破坏的场景之一,尤其是使用 props spread 时:

const Child = () => {};
const ChildMemo = React.memo(Child);

const Component = (props) => {
  return <ChildMemo {...props} />;
};

const ComponentInBetween = (props) => {
  return <Component {...props} />;
};

const InitialComponent = (props) => {
  return (
    <ComponentInBetween {...props} data={{id: '1' }} />
  );
};

请诚实回答:有多大概率,有人会在 InitialComponent 里加一个 data prop 后,去检查整条组件链里有没有 React.memo?

基本为零。

但问题是:

  • data 是 inline 对象
  • 引用每次都变
  • ChildMemo 的 memoization 被悄悄破坏

使用 React.memo 的几条规则

Rule 1: Never spread props from other components

避免这样写:

const Component = (props) => {
  return <ChildMemo {...props} />;
};

props spread 是黑盒。只要有一个不稳定值,就会破坏 memoization。

改成显式传递:

const Component = (props) => {
  return <ChildMemo some={props.some} other={props.other} />;
};

Rule 2: Avoid non-primitive props from other components

只要是对象、数组、函数,而且没 memoize,React.memo 就会失效。

一个非原始值就够了。


Rule 3: Avoid non-primitive values from custom hooks

这是最容易踩坑的一条。

const Component = () => {
  const { submit } = useForm();
  return <ChildMemo onChange={submit} />;
};

你根本不知道 submit 是否稳定。

如果 hook 内部是这样:

const useForm = () => {
  const submit = () => {};
  return { submit };
};

submit 每次 render 都是新函数。ChildMemo 的 memoization 直接失效。

React.memo 和 children

const ChildMemo = React.memo(Child);

const Component = () => {
  return (
    <ChildMemo>
      <div>Some text here</div>
    </ChildMemo>
  );
};

看起来没问题,对吧?但 memoization 又坏了。

原因是:

<ChildMemo>
  <div>Some text here</div>
</ChildMemo>

// 等价于
<ChildMemo children={<div>Some text here</div>} />

JSX element 本身就是对象。每次 render,children 都是一个新对象。

解决方案:

const Component = () => {
  const content = useMemo(() => <div>Some text here</div>, []);

  return <ChildMemo>{content}</ChildMemo>;
};

同样适用于 render props。

React.memo + memoized children(差一点)

const ChildMemo = React.memo(Child);
const ParentMemo = React.memo(Parent);

const Component = () => {
  return (
    <ParentMemo>
      <ChildMemo />
    </ParentMemo>
  );
};

这依然是错的。

因为 <ChildMemo /> 依然是一个新的 element 对象。

修复方式:

const Component = () => {
  const child = useMemo(() => <ChildMemo />, []);

  return <ParentMemo>{child}</ParentMemo>;
};

甚至你会发现:

const Component = () => {
  const child = useMemo(() => <Child />, []);

  return <ParentMemo>{child}</ParentMemo>;
};

Child 是否 memoized 已经不重要了。

useMemo 和“昂贵计算”

最后聊一个最常被滥用的场景。

什么算“昂贵”?

  • 拼接字符串?
  • 排序 300 条数据?
  • 对 5000 字文本跑正则?

答案是:不知道,除非你真的去测量。

你必须在:

  • 真实设备
  • 真实使用场景
  • 对比整体渲染成本

中去测。

很多时候:

  • JS 计算只要 2ms
  • UI re-render 却要 20ms

该优化哪个?答案显而易见。


最终规则

  • 先测量,不要假设
  • 先优化 re-render,再考虑 useMemo
  • useMemo 只在 re-render 时才有意义
  • 滥用 useMemo 会拖慢首屏渲染

一个 useMemo 不会有问题。但上百个?那就是 death by a thousand cuts

希望本文能帮助你更清晰地理解 React 中的性能优化手段。更多关于前端框架和工程化的深度讨论,欢迎访问云栈社区前端板块进行交流。记住,正确的工具用在正确的地方,才能发挥最大的价值。




上一篇:Vue3与Vue2深度源码对比:从Proxy响应式到编译优化架构演进
下一篇:红队视角下的企业安全实战:从供应链与云原生漏洞突破外网防线
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-2-4 21:41 , Processed in 0.369545 second(s), 42 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表