掘金文章归档 · 第 17 篇 React 系列 4/4 性能优化

详解 React.memo 的更新机制

基础类型看值、引用类型看地址——浅比较规则 + 5 个场景拆解重渲染时机

原文作者:随意_(掘金) | juejin.cn/post/7537220841302802442 | 发布于 2025-08-12 | 阅读 280 · 约 6 分钟

0React.memo 的基本作用

我们在开发中,对于一些开销较大且 props 不经常变动的子组件,通常使用 memo 优化处理。默认情况下,当父组件重渲染时,其所有子组件都会无条件跟随重渲染,无论子组件的 props 是否发生变化。React.memo 的作用就是:对被包裹的函数组件进行"props 浅比较",如果 props 没有变化,则阻止子组件重渲染。

// 未使用 memo:父组件重渲染时,Child 必然重渲染
const Child = (props) => { ... }

// 使用 memo:仅当 props 变化时,Child 才重渲染
const MemoizedChild = React.memo(Child);

1如何判断"props 没变化":浅比较规则

React.memo 默认使用浅比较(shallow comparison)来判断 props 是否变化:

类型比较规则示例
基本类型(string/number/boolean/null/undefined)比较是否相等count 从 5 变为 5 → 没变化;name 从 "abc" 变为 "abc" → 没变化
引用类型(object/array/function)比较引用地址是否相同内容相同但每次新建对象 → 引用变化 → 判定为变化
// 父组件
const Parent = () => {
  // 每次 Parent 重渲染,都会创建新的对象(引用地址变化)
  const user = { name: "张三" };

  return <MemoizedChild user={user} />;
};

// 子组件(被 memo 包裹)
const Child = ({ user }) => { ... };
const MemoizedChild = React.memo(Child);
⚠️ 此时,即使 user 的内容始终是 {name: "张三"},但每次父组件重渲染都会创建新对象(引用地址不同),memo 会认为 props 变化了,导致子组件仍然重渲染。

2特殊情况:函数 / 事件处理函数

函数也是引用类型,同样遵循"引用地址比较"规则。如果父组件在渲染时动态创建函数,会导致子组件 props 中函数的引用变化,进而触发重渲染:

// 父组件
const Parent = () => {
  // 每次重渲染都会创建新的函数(引用变化)
  const handleClick = () => { console.log("点击"); };

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

解决办法:使用 useCallback 缓存函数引用,确保每次渲染时函数引用不变:

import { useCallback } from 'react';

const Parent = () => {
  // 用 useCallback 缓存函数,引用地址不变
  const handleClick = useCallback(() => {
    console.log("点击");
  }, []); // 依赖为空,函数始终不变

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

3自定义比较逻辑:第二个参数

如果默认的浅比较满足不了需求(比如需要深比较对象内容),可以给 React.memo 传递第二个参数(一个比较函数),自定义判断逻辑:

// 自定义比较函数:比较 user 对象的内容是否相同
const arePropsEqual = (prevProps, nextProps) => {
  return prevProps.user.name === nextProps.user.name;
};

// 使用自定义比较函数的子组件
const MemoizedChild = React.memo(Child, arePropsEqual);
✅ 此时,即使 user 对象的引用变化,但只要 name 字段相同,子组件就不会重渲染。注意:深比较有性能成本,需权衡使用。

4五个实战场景拆解重渲染时机

场景 1:props 包含基础类型 + 对象(对象引用变化)

// 子组件(被 memo 包裹)
const MemoizedChild = React.memo(({ count, user }) => {
  console.log("子组件渲染了");
  return <div>{count} - {user.name}</div>;
});

// 父组件
const Parent = () => {
  const [count, setCount] = useState(0);
  // 每次父组件重渲染,都会创建新对象(引用变化)
  const user = { name: "张三" };

  return (
    <div>
      <button onClick={() => setCount(count + 1)}>加1</button>
      <MemoizedChild count={count} user={user} />
    </div>
  );
};
变化项判定
count(基础类型)点击按钮变化判定为「变化」
user(对象)内容不变但每次新建判定为「变化」
📌 结果:初始渲染子组件渲染;count 变化 → 子组件重渲染;父组件因其他原因重渲染但 count 不变 → 子组件仍重渲染(因 user 引用变化)。

场景 2:props 包含基础类型 + 对象(对象引用不变)

// 父组件改进:用 useMemo 缓存对象引用
const Parent = () => {
  const [count, setCount] = useState(0);
  // 用 useMemo 缓存对象,依赖为空时引用不变
  const user = useMemo(() => ({ name: "张三" }), []);

  return (
    <div>
      <button onClick={() => setCount(count + 1)}>加1</button>
      <MemoizedChild count={count} user={user} />
    </div>
  );
};
📌 结果:count 变化 → 子组件重渲染;count 不变 → 子组件不重渲染(所有 props 均未变化)。

场景 3:props 包含函数(函数引用变化)

// 子组件
const MemoizedChild = React.memo(({ onSubmit, config }) => {
  console.log("子组件渲染了");
  return <button onClick={onSubmit}>{config.text}</button>;
});

// 父组件
const Parent = () => {
  const config = useMemo(() => ({ text: "提交" }), []);
  // 每次渲染创建新函数(引用变化)
  const handleSubmit = () => {
    console.log("提交");
  };

  return <MemoizedChild onSubmit={handleSubmit} config={config} />;
};
📌 结果:父组件任何重渲染(如自身 state 变化)都会导致子组件重渲染(因 onSubmit 引用变化)。

场景 4:props 包含函数(引用不变)+ 基础类型(值不变)

// 父组件改进:用 useCallback 缓存函数
const Parent = () => {
  const [text, setText] = useState("");
  const config = useMemo(() => ({ text: "提交" }), []);

  // 用 useCallback 缓存函数,引用不变
  const handleSubmit = useCallback(() => {
    console.log("提交:", text);
  }, [text]); // 依赖 text,text 变化时函数才更新

  return <MemoizedChild onSubmit={handleSubmit} config={config} />;
};
📌 结果:text 变化 → handleSubmit 引用变化 → 子组件重渲染;text 不变 → 所有 props 均未变化 → 子组件不重渲染。

场景 5:部分 props 变化,部分不变

// 子组件
const MemoizedChild = React.memo(({ a, b, c }) => {
  console.log("子组件渲染了");
  return <div>{a} - {b.name} - {c()}</div>;
});

// 父组件
const Parent = () => {
  const [a, setA] = useState(1);
  const b = useMemo(() => ({ name: "固定值" }), []); // 不变
  const c = useCallback(() => "固定函数", []); // 不变

  return (
    <div>
      <button onClick={() => setA(a + 1)}>修改a</button>
      <MemoizedChild a={a} b={b} c={c} />
    </div>
  );
};
📌 结果:a 变化 → 子组件重渲染(只要有一个 props 变化就会触发);a 不变 → 子组件不重渲染。

5总结 & 术语表

✅ 结尾总结:
  • 「一票否决制」:props 中只要有任何一项被浅比较判定为「变化」,子组件就会重渲染。
  • 基础类型看「值」:number/string/boolean 等只要值相同,就判定为「未变化」。
  • 引用类型看「地址」:object/array/function 等即使内容相同,只要引用地址变化,就判定为「变化」。
  • 解决引用类型问题:对象/数组用 useMemo 缓存引用;函数用 useCallback 缓存引用。
  • 自定义比较逻辑:需要深比较(如比较对象内容)时,通过 React.memo 第二个参数手动实现,但需注意深比较的性能成本。

注意事项

注意点说明
只优化纯函数组件memo 仅对函数组件有效,且组件必须是「纯函数」(相同 props 始终返回相同 UI)
不影响自身 state 导致的重渲染子组件自身 state 或 context 变化时,即使 memo 包裹仍然会重渲染(memo 只处理 props 变化)
避免过度使用比较 props 本身有性能成本,渲染成本低的组件使用 memo 可能得不偿失

术语表

术语定义
HOC(高阶组件)接收组件并返回新组件的函数,React.memo 即为此类
浅比较(shallow comparison)只比较第一层:基本类型比值、引用类型比地址
useMemo缓存计算值/对象引用的 Hook,依赖不变时引用不变
useCallback缓存函数引用的 Hook,依赖不变时函数引用不变
arePropsEqualReact.memo 第二参数:自定义 props 比较函数