详解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 的第二个参数手动实现,但需注意深比较的性能成本。

相关推荐
mldong6 小时前
你的 Vue3 项目也能有钉钉同款审批流设计器:npm 装包,10 分钟画出第一条审批流
前端·vue.js
2分钟速写快排7 小时前
什么是 RAG?如何用 RAG 实现一个用户记忆?
前端·后端·ai编程
passerby60617 小时前
如何自己造一个时间处理库
前端·javascript·github
走到天涯海角8 小时前
react里面的长列表渲染优化
前端·react.js·前端框架
小羊没烦恼!8 小时前
Hello Web API系列教程——Web API与国际化
java·服务器·前端·javascript·php
北岛贰9 小时前
迷茫焦虑期,我做了一个带支付带官网的 AI 聊天虚拟恋人 App
前端·人工智能·后端
mayaairi10 小时前
Vue2 组件通讯(三):全局事件总线、PubSub、插槽与组件实例属性
前端·javascript·vue.js
kyriewen11 小时前
面试官问我:AI 都能写代码了,前端凭什么还值 25K
前端·javascript·人工智能
风骏时光牛马12 小时前
AI源码分析:拆解模型底层实现逻辑
前端
BillKu12 小时前
全局样式变量:CSS 自定义属性(--border-color)和SCSS 变量($border-color)的说明
javascript·css·scss