React.memo 与 shouldComponentUpdate 的区别是什么?如何处理深层对象比较?

大白话 React.memo 与 shouldComponentUpdate 的区别是什么?如何处理深层对象比较?

前端小伙伴们,有没有被“组件性能优化”搞到头发掉?写个列表页,改个无关状态子组件就疯狂渲染;想用优化手段,又纠结用React.memo还是shouldComponentUpdate……今天咱们就用“快递检查”和“门卫大叔”的比喻,把这俩优化工具的区别讲得明明白白,再搞定深层对象比较的痛点!

一、组件优化的"选择困难症"

先讲个我上周的真实案例:给客户做电商后台,有个OrderList组件显示订单列表,还有个SearchFilter组件处理搜索。结果发现,修改搜索关键词(父组件状态变化)时,OrderList居然跟着重新渲染!明明订单数据(props.orders)根本没变化,这不是纯浪费性能吗?

想优化吧,又犯难:

  • 函数组件该用React.memo吗?
  • 类组件是不是得用shouldComponentUpdate
  • 要是props是嵌套对象,浅比较不管用咋办?

这些问题总结成一句话:面对不同类型的组件和复杂的props结构,该选哪种优化方式?深层对象比较又该怎么处理?

二、从"快递检查"看两者的本质区别

要搞懂React.memoshouldComponentUpdate,先想象一个场景:小区的快递驿站有两种“包裹检查员”——

1. React.memo:函数组件的"智能快递柜"

React.memo是React为函数组件设计的“记忆化工具”,相当于给组件装了个“智能快递柜”:

  • 工作方式:每次父组件渲染时,它会检查新老props是否“看起来一样”(浅比较);
  • 规则:原始类型(如string/number)直接比数值,对象/数组比“包裹皮”(引用地址);
  • 结果:如果“包裹皮”没变,直接返回上次的渲染结果(不重新渲染)。

2. shouldComponentUpdate:类组件的"门卫大叔"

shouldComponentUpdate(简称SCU)是类组件的生命周期方法,相当于小区的“门卫大叔”:

  • 工作方式:每次父组件渲染触发子组件更新前,大叔会翻一遍“包裹”(对比新老propsstate);
  • 规则:返回true(允许进入,重新渲染)或false(拒绝进入,跳过渲染);
  • 自由度:可以自定义比较逻辑(比如打开“包裹”检查里面的内容)。

3. 核心区别:适用场景与控制粒度

维度 React.memo shouldComponentUpdate
组件类型 仅函数组件 仅类组件
触发时机 父组件渲染后,检查props是否变化 类组件更新前,检查props/state
比较范围 props props+state
默认逻辑 浅比较props 默认返回true(总是重新渲染)
自定义能力 可选传入比较函数 必须手动实现比较逻辑

三、代码示例:从"傻渲染"到"精准优化"

示例1:函数组件用React.memo(浅比较版)

先看函数组件未优化的情况,再用React.memo优化:

// 未优化的函数组件:每次父组件渲染都重新渲染
function OrderList({ orders }) {
  console.log("OrderList重新渲染");
  return (
    <ul>
      {orders.map(order => (
        <li key={order.id}>{order.name}</li>
      ))}
    </ul>
  );
}

// 用React.memo优化(浅比较)
const MemoizedOrderList = React.memo(OrderList);

// 父组件:每次搜索词变化触发渲染
function Parent() {
  const [search, setSearch] = useState('');
  const [orders] = useState([{ id: 1, name: '订单1' }]); // 订单数据不变

  return (
    <div>
      <input value={search} onChange={(e) => setSearch(e.target.value)} />
      <MemoizedOrderList orders={orders} />
    </div>
  );
}

效果

  • 首次渲染:控制台输出"OrderList重新渲染";
  • 修改搜索词(父组件渲染):orders的引用未变,React.memo浅比较通过,OrderList不重新渲染,无控制台输出。

示例2:类组件用shouldComponentUpdate(自定义比较)

类组件未优化时,每次父组件渲染都会触发render,用SCU手动控制:

// 未优化的类组件:每次更新都渲染
class OrderList extends React.Component {
  render() {
    console.log("OrderList重新渲染");
    return (
      <ul>
        {this.props.orders.map(order => (
          <li key={order.id}>{order.name}</li>
        ))}
      </ul>
    );
  }
}

// 用shouldComponentUpdate优化(自定义比较)
class OptimizedOrderList extends React.Component {
  // 自定义比较逻辑:仅当orders的长度变化时重新渲染
  shouldComponentUpdate(nextProps) {
    return this.props.orders.length !== nextProps.orders.length;
  }

  render() {
    console.log("OrderList重新渲染");
    return (
      <ul>
        {this.props.orders.map(order => (
          <li key={order.id}>{order.name}</li>
        ))}
      </ul>
    );
  }
}

效果

  • 修改搜索词(父组件渲染):orders长度未变,SCU返回falserender不执行;
  • 添加新订单(orders长度变化):SCU返回true,触发render

示例3:深层对象比较(踩坑+解决方案)

props是深层对象(如{ user: { name: '张三' } }),浅比较会失效(即使内容没变,引用变了)。这时候需要手动处理:

踩坑现场:浅比较失效
// 父组件:每次渲染重新创建user对象(引用变化)
function Parent() {
  const [count, setCount] = useState(0);
  const user = { name: '张三', address: { city: '北京' } }; // 每次渲染新对象

  return (
    <div>
      <button onClick={() => setCount(c => c + 1)}>点击次数:{count}</button>
      <MemoizedUserCard user={user} />
    </div>
  );
}

// 子组件:用React.memo包裹(浅比较)
const MemoizedUserCard = React.memo(({ user }) => {
  console.log("UserCard重新渲染");
  return (
    <div>
      <p>姓名:{user.name}</p>
      <p>城市:{user.address.city}</p>
    </div>
  );
});

问题:点击按钮时,user被重新创建(引用变化),React.memo浅比较失败,UserCard重新渲染。

解决方案:深比较+useMemo

lodash.isEqual深比较,或用useMemo稳定引用:

// 方案1:自定义比较函数(深比较)
import isEqual from 'lodash.isEqual';

const MemoizedUserCard = React.memo(
  ({ user }) => { /* 渲染逻辑 */ },
  (prevProps, nextProps) => {
    // 用lodash的isEqual进行深比较
    return isEqual(prevProps.user, nextProps.user);
  }
);

// 方案2:用useMemo稳定对象引用(推荐)
function Parent() {
  const [count, setCount] = useState(0);
  // 仅当name或city变化时,才重新创建user对象
  const user = useMemo(() => ({
    name: '张三',
    address: { city: '北京' }
  }), []); // 依赖数组为空:只创建一次

  return (
    <div>
      <button onClick={() => setCount(c => c + 1)}>点击次数:{count}</button>
      <MemoizedUserCard user={user} />
    </div>
  );
}

四、不同优化方式的"优缺点"

用表格总结React.memoshouldComponentUpdate的适用场景和注意事项:

优化方式 优点 缺点 适用场景
React.memo 语法简洁,函数组件专用 默认浅比较,复杂对象需额外处理 函数组件,props为简单类型或稳定引用的对象
shouldComponentUpdate 类组件专用,可控制props+state,自定义逻辑灵活 需手动实现比较逻辑,代码量较大 类组件,需精细控制state或深层props变化
深比较(如lodash.isEqual) 准确判断深层对象变化 性能开销大(递归比较),可能比重新渲染更慢 极少数必须深层比较的场景(如嵌套层级少)
useMemo稳定引用 性能最优(避免深比较) 需正确管理依赖数组 函数组件,props为复杂对象/数组

五、面试题回答方法

正常回答(结构化):

React.memoshouldComponentUpdate的核心区别体现在以下方面:

  1. 组件类型React.memo用于函数组件,shouldComponentUpdate用于类组件;
  2. 比较范围React.memo仅比较propsshouldComponentUpdate同时比较propsstate
  3. 默认行为React.memo默认浅比较propsshouldComponentUpdate默认返回true(总是更新);
  4. 自定义能力React.memo可选传入比较函数,shouldComponentUpdate必须手动实现逻辑。
    处理深层对象比较时,推荐用useMemo稳定对象引用(性能最优),或用lodash.isEqual深比较(需谨慎,避免性能问题)。”

大白话回答(接地气):

React.memo就像函数组件的‘快递柜’——只看包裹的‘外皮’(引用地址),外皮没变就直接取上次的快递。shouldComponentUpdate是类组件的‘门卫大叔’——不仅看外皮,还能打开包裹检查里面(自定义比较逻辑),甚至连你家的狗(state)有没有变都要问。
要是包裹里套包裹(深层对象),快递柜的外皮检查就不准了。这时候要么用useMemo给包裹上把锁(稳定引用),要么让门卫大叔仔细翻(深比较),但翻太仔细会慢,所以尽量少用~”

六、总结:3个选择原则+2个深层比较建议

3个选择原则:

  1. 函数组件用React.memo:语法简单,默认浅比较足够处理大部分场景;
  2. 类组件用shouldComponentUpdate:需要控制state或复杂props时,手动实现比较逻辑;
  3. 优先稳定引用:用useMemo/useCallback避免对象/函数频繁创建,减少浅比较失败。

2个深层比较建议:

  • 能不用就不用:深比较(如lodash.isEqual)的递归逻辑可能比重新渲染更慢,尤其对大对象;
  • 用Immer不可变数据:通过Immer生成不可变对象,修改时返回新引用,配合useMemo稳定未修改部分的引用:
    import { produce } from 'immer';
    
    // 修改深层属性时,用Immer生成新对象
    const newUser = produce(oldUser, draft => {
      draft.address.city = '上海';
    });
    

七、扩展思考:4个高频问题解答

问题1:React.memo能比较state吗?

解答:不能!React.memo仅比较props,类组件的shouldComponentUpdate才能比较state。如果函数组件需要根据state优化,需将state作为props传递(或用useMemo缓存渲染逻辑)。

问题2:函数组件如何模拟shouldComponentUpdate?

解答:函数组件可以通过React.memo+自定义比较函数,模拟shouldComponentUpdateprops比较逻辑。如果需要比较state,需将state作为props传递给自身(不推荐,会增加耦合)。

问题3:深比较一定准确吗?

解答:不一定!深比较无法处理循环引用的对象(会导致无限递归),且对Date/RegExp等特殊对象的比较可能不符合预期(lodash.isEqual已处理这些情况)。

问题4:React.memo会影响useEffect吗?

解答:不影响!React.memo仅阻止组件重新渲染,不影响useEffect的执行(useEffect依赖数组变化时仍会触发)。

结尾:优化不是玄学,理解原理是关键

React.memoshouldComponentUpdate是React性能优化的“左右护法”,但选择哪一个、怎么用,关键在理解它们的“脾气”——函数组件用React.memo,类组件用shouldComponentUpdate,深层对象尽量用useMemo稳定引用。

记住:优化的目标是“该渲染时渲染,不该渲染时不渲染”,而不是盲目追求所有组件都不渲染。下次遇到组件性能问题,你就能自信地说:“我知道该用哪个工具!”

如果这篇文章帮你理清了思路,记得点个赞,咱们下期聊,不见不散~

你可能感兴趣的:(大白话前端八股,react.js,javascript,ecmascript)