详解React.memo的更新机制

背景

我们在开发中,对于一些开销较大且props不经常变动的子组件,通常使用的 memo优化处理,但是真的了解memo的更新时机吗,下面基于 基础类型 引用类型 详细解释下 添加了memo的子组件更新时机

React.memo介绍

在 React 中,React.memo 是一个用于优化函数组件性能的高阶组件(HOC),其核心作用是减少不必要的重渲染。理解它的工作机制需要从「props 比较逻辑」和「重渲染触发条件」两方面展开

一、React.memo 的基本作用

默认情况下,当父组件重渲染时,其所有子组件都会无条件跟随重渲染,无论子组件的 props 是否发生变化。这在某些场景下会造成性能浪费(比如子组件渲染成本高,且 props 很少变化)。

React.memo 的作用就是:对被包裹的函数组件进行「props 浅比较」,如果 props 没有变化,则阻止子组件重渲染

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

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

二、关键问题:如何判断「props 没变化」?

React.memo 默认使用「浅比较(shallow comparison)」来判断 props 是否变化,具体规则如下:

1. 对于基本类型(string/number/boolean/null/undefined)

比较的是值是否相等。例如:

  • props.count5 变为 5 → 认为没变化(不重渲染)
  • props.name"abc" 变为 "abc" → 认为没变化(不重渲染)
2. 对于引用类型(object/array/function 等)

比较的是引用地址是否相同,而不是内容是否相同。例如

js 复制代码
// 父组件
const Parent = () => {
  // 每次 Parent 重渲染,都会创建新的对象(引用地址变化)
  const user = { name: "张三" }; 
  
  return <MemoizedChild user={user} />;
};

// 子组件(被 memo 包裹)
const Child = ({ user }) => { ... };
const MemoizedChild = React.memo(Child);

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

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

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

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

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

javascript 复制代码
import { useCallback } from 'react';

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

四、自定义比较逻辑

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

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

// 使用自定义比较函数的子组件
const MemoizedChild = React.memo(Child, arePropsEqual);

此时,即使 user 对象的引用变化,但只要 name 字段相同,子组件就不会重渲染。

五、注意事项

  1. 只优化纯函数组件memo 仅对函数组件有效,且组件必须是「纯函数」(相同 props 始终返回相同 UI)。
  2. 不影响自身 state 导致的重渲染 :如果子组件自身的 statecontext 变化,即使 memo 包裹,仍然会重渲染(memo 只处理 props 变化)。
  3. 避免过度使用 :比较 props 本身有性能成本,对于渲染成本低的组件,使用 memo 可能得不偿失。

使用场景

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

javascript 复制代码
// 子组件(被 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 是基础类型,当点击按钮时 count 值变化 → 判定为「变化」
  • user 是对象,即使内容不变,每次父组件渲染都会创建新对象(引用变化)→ 判定为「变化」

结果

  • 初始渲染:子组件渲染
  • 点击按钮(count 变化):子组件重渲染(因 count 变化)
  • 父组件因其他原因重渲染(如父组件自身 state 变化但 count 不变):子组件仍重渲染(因 user 引用变化)

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

js 复制代码
// 父组件改进:用 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 变化时 → 判定为「变化」
  • useruseMemo 缓存,引用始终不变 → 判定为「未变化」

结果

  • count 变化时:子组件重渲染(因 count 变化)
  • count 不变时:子组件不重渲染(所有 props 均未变化)

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

js 复制代码
// 子组件
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} />;
};

变化分析

  • config 被缓存,引用不变 → 判定为「未变化」
  • handleSubmit 每次渲染都是新函数(引用变化)→ 判定为「变化]

结果

  • 父组件任何重渲染(如自身 state 变化)都会导致子组件重渲染(因 onSubmit 引用变化)

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

arduino 复制代码
// 父组件改进:用 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} />;
};

变化分析

  • config 引用不变 → 「未变化」
  • handleSubmit 仅当 text 变化时引用才变化 → 其他时候「未变化」

结果

  • text 变化时:handleSubmit 引用变化 → 子组件重渲染
  • text 不变时:所有 props 均未变化 → 子组件不重渲染

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

javascript 复制代码
// 子组件
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 变化时 → 「变化」
  • bc 始终不变 → 「未变化]

结果

  • 点击按钮(a 变化):子组件重渲染(只要有一个 props 变化就会触发)
  • 其他情况(a 不变):子组件不重渲染

结尾总结

  1. 「一票否决制」props 中只要有任何一项被浅比较判定为「变化」,子组件就会重渲染。

  2. 基础类型看「值」number/string/boolean 等只要值相同,就判定为「未变化」。

  3. 引用类型看「地址」object/array/function 等即使内容相同,只要引用地址变化,就判定为「变化」。

  4. 解决引用类型问题

    • 对象 / 数组:用 useMemo 缓存引用
    • 函数:用 useCallback 缓存引用
  5. 自定义比较逻辑 :如果需要深比较(如比较对象内容),可通过 React.memo 的第二个参数手动实现,但需注意深比较的性能成本。

相关推荐
姑苏洛言14 分钟前
有趣的 npm 库 · json-server
前端
知否技术18 分钟前
Vue3项目中轻松开发自适应的可视化大屏!附源码!
前端·数据可视化
Hilaku20 分钟前
为什么我坚持用git命令行,而不是GUI工具?
前端·javascript·git
用户adminuser22 分钟前
深入理解 JavaScript 中的闭包及其实际应用
前端
heartmoonq24 分钟前
个人对于sign的理解
前端
ZzMemory24 分钟前
告别移动端适配烦恼!pxToViewport 凭什么取代 lib-flexible?
前端·css·面试
Running_C28 分钟前
从「救命稻草」到「甜蜜的负担」:我对 TypeScript 的爱恨情仇
前端·typescript
狂炫一碗大米饭1 小时前
如何优化vue中的渲染🔒🔑🔓
vue.js
前端搬运侠1 小时前
📝从零到一封装 React 表格:基于 antd Table 实现多条件搜索 + 动态列配置,代码可直接复用
前端
歪歪1001 小时前
Vue原理与高级开发技巧详解
开发语言·前端·javascript·vue.js·前端框架·集成学习