MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

React 使用 memo 优化 Context 子组件更新

2024-07-267.4k 阅读

React Context 基础

在深入探讨如何使用 memo 优化 Context 子组件更新之前,我们先来回顾一下 React Context 的基本概念。

React Context 是一种共享数据的方式,它允许我们在组件树中传递数据,而无需在每一层手动传递 props。这在处理一些全局数据,如用户认证信息、主题设置等场景下非常有用。

创建 Context

首先,我们使用 createContext 方法来创建一个 Context 对象。

import React from 'react';

// 创建一个 Context 对象
const ThemeContext = React.createContext();

export default ThemeContext;

提供 Context

然后,我们需要通过 Provider 组件来提供 Context。任何在 Provider 组件树内的组件都可以访问到这个 Context。

import React from 'react';
import ThemeContext from './ThemeContext';

const App = () => {
  const theme = 'dark';

  return (
    <ThemeContext.Provider value={theme}>
      {/* 应用的其他组件 */}
    </ThemeContext.Provider>
  );
};

export default App;

消费 Context

有几种方式可以消费 Context。一种是使用 Context.Consumer 组件,另一种是使用 useContext Hook。

使用 Context.Consumer 组件

import React from'react';
import ThemeContext from './ThemeContext';

const Button = () => {
  return (
    <ThemeContext.Consumer>
      {theme => (
        <button style={{ background: theme === 'dark'? 'black' : 'white', color: theme === 'dark'? 'white' : 'black' }}>
          Click me
        </button>
      )}
    </ThemeContext.Consumer>
  );
};

export default Button;

使用 useContext Hook

import React, { useContext } from'react';
import ThemeContext from './ThemeContext';

const Button = () => {
  const theme = useContext(ThemeContext);

  return (
    <button style={{ background: theme === 'dark'? 'black' : 'white', color: theme === 'dark'? 'white' : 'black' }}>
      Click me
    </button>
  );
};

export default Button;

Context 引发的子组件更新问题

虽然 React Context 提供了一种便捷的共享数据方式,但它也带来了一些性能问题。具体来说,当 Context 的值发生变化时,所有消费该 Context 的组件都会重新渲染,即使它们的 props 并没有实际改变。

假设我们有一个复杂的组件树,其中一些深层子组件依赖于 Context,但它们自身的逻辑并不依赖于 Context 值的频繁变化。例如,一个展示用户信息的子组件可能只在用户登录或登出时需要更新,而不是每次用户偏好设置(如主题切换)发生变化时都更新。

import React, { useContext } from'react';
import ThemeContext from './ThemeContext';

// 一个深层子组件
const DeepChildComponent = () => {
  const theme = useContext(ThemeContext);
  console.log('DeepChildComponent re - rendered');

  return (
    <div>
      <p>Some content in deep child. Theme: {theme}</p>
    </div>
  );
};

export default DeepChildComponent;

在上述代码中,每次 ThemeContext 的值发生变化,DeepChildComponent 都会重新渲染,即使它内部的逻辑并不依赖于主题的频繁变化。这在大型应用中可能会导致性能问题,因为不必要的重新渲染会浪费计算资源。

React memo 简介

React.memo 是 React 提供的一个高阶组件(HOC),用于对函数组件进行浅比较优化。它可以避免组件在 props 没有变化时进行不必要的重新渲染。

使用 React memo

基本使用非常简单,只需要将 React.memo 包裹在函数组件外面。

import React from'react';

const MyComponent = ({ value }) => {
  return <div>{value}</div>;
};

export default React.memo(MyComponent);

在上述代码中,MyComponent 只有在 props.value 发生变化时才会重新渲染。如果 props.value 保持不变,即使父组件重新渲染,MyComponent 也不会重新渲染。

自定义比较函数

默认情况下,React.memo 使用浅比较来判断 props 是否发生变化。但有时候,浅比较可能不够,我们需要自定义比较逻辑。这时,可以传递一个比较函数作为第二个参数。

import React from'react';

const MyComponent = ({ complexObject }) => {
  return <div>{JSON.stringify(complexObject)}</div>;
};

const arePropsEqual = (prevProps, nextProps) => {
  return JSON.stringify(prevProps.complexObject) === JSON.stringify(nextProps.complexObject);
};

export default React.memo(MyComponent, arePropsEqual);

在这个例子中,我们通过 arePropsEqual 函数自定义了比较逻辑,用于比较复杂对象 complexObject 是否发生变化。

使用 memo 优化 Context 子组件更新

现在,我们结合 React.memo 来优化 Context 子组件的更新。

优化简单子组件

对于像前面提到的 DeepChildComponent 这样简单依赖 Context 的子组件,我们只需要用 React.memo 包裹它。

import React, { useContext } from'react';
import ThemeContext from './ThemeContext';

// 一个深层子组件
const DeepChildComponent = () => {
  const theme = useContext(ThemeContext);
  console.log('DeepChildComponent re - rendered');

  return (
    <div>
      <p>Some content in deep child. Theme: {theme}</p>
    </div>
  );
};

export default React.memo(DeepChildComponent);

这样,只有当 ThemeContext 的值发生变化且 DeepChildComponent 从 Context 中获取的值也发生变化时,DeepChildComponent 才会重新渲染。如果 ThemeContext 的值变化,但 DeepChildComponent 从 Context 中获取的值没有变化(例如,主题切换但该组件只关心主题的一部分且这部分未变),它不会重新渲染。

处理复杂依赖

有时候,子组件可能依赖于 Context 中的复杂对象,并且对象内部的部分属性变化可能并不需要组件重新渲染。这时,我们可以结合自定义比较函数来优化。

假设我们的 Context 提供了一个用户对象,子组件只关心用户的姓名,而不关心其他属性(如年龄、地址等)。

import React, { useContext } from'react';
import UserContext from './UserContext';

const UserNameComponent = () => {
  const user = useContext(UserContext);
  console.log('UserNameComponent re - rendered');

  return (
    <div>
      <p>User name: {user.name}</p>
    </div>
  );
};

const arePropsEqual = (prevProps, nextProps) => {
  return prevProps.user.name === nextProps.user.name;
};

export default React.memo(UserNameComponent, arePropsEqual);

在这个例子中,UserNameComponent 只有在 UserContext 中的用户姓名发生变化时才会重新渲染,而不是每次用户对象整体发生变化时都重新渲染。

嵌套 Context 场景

在实际应用中,可能会遇到多个嵌套的 Context。例如,我们有一个主题 Context 和一个用户认证 Context。

import React, { useContext } from'react';
import ThemeContext from './ThemeContext';
import AuthContext from './AuthContext';

const NestedContextComponent = () => {
  const theme = useContext(ThemeContext);
  const isAuthenticated = useContext(AuthContext);
  console.log('NestedContextComponent re - rendered');

  return (
    <div>
      <p>Theme: {theme}, Authenticated: {isAuthenticated? 'Yes' : 'No'}</p>
    </div>
  );
};

const arePropsEqual = (prevProps, nextProps) => {
  return prevProps.theme === nextProps.theme && prevProps.isAuthenticated === nextProps.isAuthenticated;
};

export default React.memo(NestedContextComponent, arePropsEqual);

在这个例子中,NestedContextComponent 依赖于两个 Context。通过自定义比较函数,我们确保只有当这两个 Context 中它所依赖的值发生变化时,组件才会重新渲染。

注意事项

  1. Context 值的稳定性:尽量确保 Context 的值不会频繁变化。如果 Context 的值频繁变化,即使使用 memo 优化,仍然可能导致不必要的重新渲染。例如,避免在 Provider 组件的 value 属性中传递新创建的对象或函数。
// 不好的做法
const App = () => {
  const theme = {
    color: 'black',
    fontSize: '16px'
  };

  return (
    <ThemeContext.Provider value={theme}>
      {/* 应用的其他组件 */}
    </ThemeContext.Provider>
  );
};

// 好的做法
const theme = {
  color: 'black',
  fontSize: '16px'
};

const App = () => {
  return (
    <ThemeContext.Provider value={theme}>
      {/* 应用的其他组件 */}
    </ThemeContext.Provider>
  );
};
  1. 深层对象比较:自定义比较函数时,要注意深层对象的比较。JSON.stringify 虽然可以用于简单的深层对象比较,但它有局限性,比如不能处理函数、Date 对象等。对于更复杂的深层对象比较,可能需要使用专门的库,如 lodash.isEqual
import React from'react';
import { isEqual } from 'lodash';

const MyComponent = ({ complexObject }) => {
  return <div>{JSON.stringify(complexObject)}</div>;
};

const arePropsEqual = (prevProps, nextProps) => {
  return isEqual(prevProps.complexObject, nextProps.complexObject);
};

export default React.memo(MyComponent, arePropsEqual);
  1. 性能权衡:虽然 memo 可以优化组件的重新渲染,但过多使用自定义比较函数可能会带来额外的性能开销。在实际应用中,需要根据具体场景进行性能测试和权衡。

结合其他优化策略

  1. useCallback 和 useMemouseCallbackuseMemo 可以与 React.memo 配合使用,进一步优化性能。useCallback 用于缓存函数,useMemo 用于缓存值,这样可以避免在每次渲染时重新创建函数和对象,从而减少不必要的重新渲染。
import React, { useCallback, useMemo } from'react';

const ParentComponent = () => {
  const data = [1, 2, 3, 4, 5];

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

  const processedData = useMemo(() => {
    return data.map(num => num * 2);
  }, [data]);

  return (
    <div>
      <ChildComponent data={processedData} onClick={handleClick} />
    </div>
  );
};

const ChildComponent = React.memo(({ data, onClick }) => {
  return (
    <div>
      <button onClick={onClick}>Click me</button>
      <ul>
        {data.map(num => (
          <li key={num}>{num}</li>
        ))}
      </ul>
    </div>
  );
});

export default ParentComponent;

在这个例子中,handleClick 函数通过 useCallback 缓存,processedData 通过 useMemo 缓存。ChildComponent 使用 React.memo,只有当 dataonClick 函数发生变化时才会重新渲染。

  1. shouldComponentUpdate:对于类组件,shouldComponentUpdate 方法可以实现类似 React.memo 的功能,用于控制组件是否应该重新渲染。虽然 React 推荐使用函数组件和 React.memo,但在一些遗留代码中,仍然可能会用到 shouldComponentUpdate
import React from'react';

class MyClassComponent extends React.Component {
  shouldComponentUpdate(nextProps, nextState) {
    return this.props.value!== nextProps.value;
  }

  render() {
    return <div>{this.props.value}</div>;
  }
}

export default MyClassComponent;

实际应用案例分析

假设我们正在开发一个电商应用,其中有一个购物车功能。购物车组件依赖于一个全局的用户认证 Context 和一个购物车数据 Context。

创建 Context

import React from'react';

const AuthContext = React.createContext();
const CartContext = React.createContext();

export { AuthContext, CartContext };

提供 Context

import React from'react';
import { AuthContext, CartContext } from './Contexts';

const App = () => {
  const isAuthenticated = true;
  const cart = [
    { id: 1, name: 'Product 1', price: 10 },
    { id: 2, name: 'Product 2', price: 20 }
  ];

  return (
    <AuthContext.Provider value={isAuthenticated}>
      <CartContext.Provider value={cart}>
        {/* 应用的其他组件 */}
      </CartContext.Provider>
    </AuthContext.Provider>
  );
};

export default App;

购物车子组件

import React, { useContext } from'react';
import { AuthContext, CartContext } from './Contexts';

const CartItem = React.memo(({ item }) => {
  const isAuthenticated = useContext(AuthContext);
  return (
    <li>
      {item.name} - ${item.price} {isAuthenticated? 'You can checkout' : 'Please login to checkout'}
    </li>
  );
});

const CartList = () => {
  const cart = useContext(CartContext);
  return (
    <ul>
      {cart.map(item => (
        <CartItem key={item.id} item={item} />
      ))}
    </ul>
  );
};

export default CartList;

在这个案例中,CartItem 组件使用 React.memo 进行优化。只有当 item props 或 AuthContext 的值发生变化时,CartItem 才会重新渲染。这样,当购物车中其他商品的数量或价格发生变化(即 CartContext 的值整体变化,但当前 CartItemitem 未变)时,CartItem 不会不必要地重新渲染。

总结优化步骤

  1. 确定依赖:分析子组件依赖于 Context 中的哪些值。
  2. 包裹 memo:用 React.memo 包裹依赖 Context 的子组件。
  3. 自定义比较(如有需要):如果默认的浅比较不够,根据子组件实际依赖的 Context 值,编写自定义比较函数。
  4. 确保 Context 值稳定:避免在 Provider 中频繁创建新的对象或函数作为 value
  5. 结合其他优化:配合 useCallbackuseMemo 等进行全面的性能优化。

通过以上步骤,可以有效地使用 React.memo 优化 Context 子组件的更新,提高 React 应用的性能。在实际开发中,要根据具体的应用场景和性能需求,灵活运用这些优化策略。同时,不断进行性能测试,确保优化措施真正起到提升性能的作用。

希望通过本文的介绍和示例,你对如何使用 React.memo 优化 Context 子组件更新有了更深入的理解和掌握。在日常开发中,合理运用这些技术可以使我们的 React 应用更加高效和流畅。