Coding Hao

详解React高阶组件(HOC)之React.memo

2024年6月15日 (1年前)

介绍

React.memo 是 React 提供的一个高阶组件(Higher-Order Component, HOC),用于优化函数组件的渲染性能。它通过 浅比较 组件的 props 来决定是否重新渲染组件。如果 props 没有变化,组件将跳过渲染,直接复用上一次的渲染结果。 通俗点说就是当父组件重新渲染时,如果 memo 包装的子组件的 props 没有发生变化,React 将跳过这个子组件的渲染,直接复用上一次的渲染结果。这可以避免不必要的重新渲染,提高性能。 使用场景:

  • 当组件经常接收相同的 props 但父组件频繁更新时
  • 纯展示型组件,渲染开销较大时
  • 需要避免子组件无谓渲染的场景

重新渲染 render 会做些什么?会对新旧 VNode 进行对比,也就是我们所说的 Dom diff。对新旧两棵树进行一个深度优先遍历,这样每一个节点都会一个标记,在到深度遍历的时候,每遍历到一和个节点,就把该节点和新的节点树进行对比,如果有差异就放到一个对象里面,遍历差异对象,根据差异的类型,根据对应对规则更新 VNode

基本用法

React.memo 接受一个函数组件作为参数,并返回一个记忆化(memoized)的组件。

const MemoizedComponent = React.memo(Component, arePropsEqual?);
  • Component:需要优化的函数组件
  • arePropsEqual: (可选):自定义比较函数,用于决定是否重新渲染组件。默认情况下,React.memo 会对 props 进行浅比较。

案例

以下代码未使用React.memo:

import React, { useState } from 'react';

function Child({ name }) {
  console.log('Child rendered');
  return <div>{name}</div>;
}

function Parent() {
  const [count, setCount] = useState(0);
  return (
    <div>
      <button onClick={()=> setCount(count + 1)}>Increment</button>
      <p>Count: {count}</p>
      <Child name="Alice" />
    </div>
  );
}
export default Parent;

以上代码会出现什么问题呢?那就是每次点击按钮时,Parent 组件会重新渲染,导致 Child 组件也重新渲染,即使 name 没有变化。

下面我们就使用 React.memo 来进行优化:

import React, { useState, memo } from 'react';

const Child = memo(function Child({ name }) {
  console.log('Child rendered');
  return <div>{name}</div>;
});

function Parent() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <button onClick={()=> setCount(count + 1)}>Increment</button>
      <p>Count: {count}</p>
      <Child name="Alice" />
    </div>
  );
}

export default Parent;
  • 效果:Child 组件只在 name 变化时重新渲染。点击按钮时,Child 不会重新渲染。

自定义比较函数

默认情况下,React.memo 会对 props 进行浅比较。如果 props 是复杂对象或数组,浅比较可能无法满足需求。此时可以提供一个自定义的比较函数。 ** 语法 **

const MemoizedComponent = React.memo(Component, (prevProps, nextProps) => {
  // 返回 true 表示 props 相等,不重新渲染
  // 返回 false 表示 props 不相等,重新渲染
});

示例:

import React, { useState, memo, useCallback } from 'react';
const Child = memo(
  function Child({ user }) {
    console.log('Child rendered');
    return <div>{user.name}</div>;
  },
  (prevProps, nextProps) => {
    // 只有当 user.name 变化时才重新渲染
    return prevProps.user.name === nextProps.user.name;
  }
);

注意事项

  • 浅比较的限制:
    • React.memo 默认使用浅比较,如果 props 是对象或数组,浅比较可能无法检测到内部变化。
    • 对于复杂 props,建议使用自定义比较函数。
  • 不要滥用 React.memo
    • 对于简单组件,React.memo 可能不会带来明显的性能提升,反而会增加比较的开销, 因为比较 props 本身也需要计算开销,如果组件经常接收不同的 props,使用 memo 反而会增加额外开销
    • 只有在组件渲染开销较大或 props 变化较少时,才建议使用 React.memo。
  • useMemouseCallback 结合使用:
    • 如果 props 包含函数或复杂对象,可以使用 useCallbackuseMemo 来避免不必要的重新创建。
import React, { useState, memo, useCallback } from 'react';

const Child = memo(function Child({ onClick }) {
  console.log('Child rendered');
  return <button onClick={onClick}>Click Me</button>;
});

function Parent() {
  const [count, setCount] = useState(0);

  const handleClick = useCallback(() => {
    console.log('Button clicked');
  }, []);

  return (
    <div>
      <button onClick={()=> setCount(count + 1)}>Increment</button>
      <p>Count: {count}</p>
      <Child onClick={handleClick} />
    </div>
  );
}

export default Parent;

注意: React.memo 和 useMemo 虽然都用于优化性能,但它们的使用场景和作用有明显区别:React.memo 用于优化整个组件的重新渲染,useMemo 用于优化组件内部特定的计算结果或值

总结

  • React.memo 用于优化函数组件的渲染性能,避免不必要的重新渲染。
  • 默认情况下,它会对 props 进行浅比较。如果需要更复杂的比较逻辑,可以提供一个自定义比较函数。
  • 结合 useCallbackuseMemo 可以进一步优化性能。