文章目录

React useCallback配合memo避免子组件无意义重渲染

发布于 2026-04-30 05:20:09 · 浏览 2 次 · 评论 0 条

React useCallback配合memo避免子组件无意义重渲染

在React应用开发中,父组件的状态更新往往会触发所有子组件的重渲染,即使子组件的props并没有发生变化。这种“无意义重渲染”会消耗宝贵的计算资源,导致页面卡顿。通过结合 React.memouseCallback,可以精准控制组件的更新时机,提升应用性能。


1. 复现性能瓶颈场景

首先,构建 一个包含父子组件的示例,直观地感受无意义的重渲染。

创建 一个名为 ParentComponent.js 的文件。在这个组件中,定义 一个 count 状态,并实现 一个按钮来更新 该状态。同时,引入 一个子组件 ChildComponent,并向其传递 一个 onClick 回调函数。

import React, { useState } from 'react';

// 子组件定义
const ChildComponent = ({ onClick }) => {
  console.log('子组件渲染了...');
  return (
    <div style={{ border: '1px solid blue', padding: '10px', margin: '10px' }}>
      <p>我是子组件</p>
      <button onClick={onClick}>子组件按钮</button>
    </div>
  );
};

// 父组件定义
const ParentComponent = () => {
  const [count, setCount] = useState(0);

  // 直接定义的内联函数
  const handleClick = () => {
    console.log('按钮被点击');
  };

  return (
    <div style={{ padding: '20px' }}>
      <h1>父组件计数: {count}</h1>
      <button onClick={() => setCount(count + 1)}>增加计数</button>

      {/* 传递给子组件 */}
      <ChildComponent onClick={handleClick} />
    </div>
  );

export default ParentComponent;

运行 代码并打开 浏览器控制台。每点击 一次“增加计数”按钮,你会发现控制台输出了“子组件渲染了...”。

分析 原因:

  1. 父组件 count 状态改变,触发父组件重渲染。
  2. 父组件重渲染时,handleClick 函数被重新创建(在JavaScript中,function() {} !== function() {})。
  3. 子组件接收到的 onClick props 引用发生了变化。
  4. React 认为数据变了,于是强制 子组件重渲染。

2. 初步尝试:使用 React.memo

为了阻止子组件因父组件状态变化而被动渲染,使用 React.memo包裹 子组件。这是一个高阶组件,它会对 props 进行浅比较。

修改 ChildComponent 的导出方式:

// 使用 React.memo 包裹
const MemoizedChildComponent = React.memo(ChildComponent);

export default MemoizedChildComponent;

保存 代码并再次测试。你会发现,点击“增加计数”按钮时,子组件依然在渲染。

探究 失败原因:
React.memo 默认通过浅比较(即 ===)来判断 props 是否变化。虽然函数的内容没变,但父组件每次渲染都会生成一个新的函数引用。对于 React 来说,onClick 的新引用不等于旧引用,因此浅比较失败,React.memo 失效。


3. 核心解决方案:引入 useCallback

要解决引用变化的问题,需要使用 useCallback Hook 来缓存 函数引用。useCallback 会在依赖项不变的情况下,返回上一次渲染中缓存的函数引用。

重构 父组件中的 handleClick 函数:

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

const ParentComponent = () => {
  const [count, setCount] = useState(0);

  // 使用 useCallback 包裹函数
  // 第二个参数是依赖数组,这里 handleClick 不依赖任何外部变量
  const handleClick = useCallback(() => {
    console.log('按钮被点击');
  }, []); 

  return (
    <div style={{ padding: '20px' }}>
      <h1>父组件计数: {count}</h1>
      <button onClick={() => setCount(count + 1)}>增加计数</button>

      {/* 这里的 onClick 引用在 count 变化时保持不变 */}
      <MemoizedChildComponent onClick={handleClick} />
    </div>
  );
};

刷新 页面并点击 “增加计数”按钮。此时,控制台不再输出“子组件渲染了...”。

验证 效果:

  1. 父组件因 count 变化而重渲染。
  2. React 检查 useCallback 的依赖数组 []。由于依赖为空且未变,useCallback 返回 上一次缓存的 handleClick 函数引用。
  3. MemoizedChildComponent 接收到的 onClick prop 与之前相同。
  4. 浅比较通过,子组件跳过 渲染。

4. 处理依赖项:何时更新函数

在实际开发中,回调函数往往需要使用父组件的状态或 props。此时,必须将使用的变量加入 useCallback 的依赖数组中。

修改 场景:假设 handleClick 需要使用父组件的 count 变量。

const ParentComponent = () => {
  const [count, setCount] = useState(0);

  // 将 count 加入依赖数组
  const handleClick = useCallback(() => {
    console.log(`当前计数是: ${count}`);
  }, [count]); 

  // ...其余代码

测试 行为:

  1. 点击 “增加计数”按钮,count 变化。
  2. useCallback 检测 到依赖项 count 发生了变化。
  3. useCallback 创建 一个新的函数引用(其中闭包包含了新的 count 值)。
  4. 子组件接收到新的 onClick 引用。
  5. 子组件触发 重渲染。

这是符合预期的行为,因为子组件的逻辑依赖于父组件的 count,它必须更新以获取最新数据。


5. 进阶注意事项:避免对象/数组的引用变化

即使使用了 useCallbackReact.memo,如果传递给子组件的 props 中包含对象数组,依然可能导致重渲染问题。

查看 常见错误代码:

const ParentComponent = () => {
  // ...
  // 每次渲染都会创建一个新的对象字面量
  const userInfo = { name: 'Alex', age: 18 };

  return (
    // ...
    <MemoizedChildComponent onClick={handleClick} user={userInfo} />
  );
};

在这个例子中,虽然 handleClick 被缓存了,但 user 对象每次渲染都是一个新的引用,导致 React.memo 失效。

解决 方法:使用 useMemo缓存 对象或数组。

import { useMemo } from 'react';

const ParentComponent = () => {
  // ...

  // 使用 useMemo 缓存对象
  const userInfo = useMemo(() => ({ 
    name: 'Alex', 
    age: 18 
  }), []); // 空依赖数组表示对象结构永不变化

  return (
    // ...
    <MemoizedChildComponent onClick={handleClick} user={userInfo} />
  );
};

6. 策略对比与总结

为了帮助快速判断何时使用这些优化手段,请参考下表。

场景描述 父组件更新时子组件行为 优化方案
无优化<br>(直接传递内联函数和组件) 总是重渲染
仅 React.memo<br>(传递内联函数) 总是重渲染<br>(因函数引用变化) 无效
React.memo + useCallback<br>(依赖为空) 不重渲染<br>(函数引用稳定) 推荐<br>(用于纯UI组件交互)
React.memo + useCallback<br>(依赖包含状态) 状态相关时重渲染<br>(逻辑需要最新数据) 推荐<br>(用于依赖数据的回调)
传递对象/数组 props 即使函数缓存也会重渲染<br>(对象引用变化) 必须配合 useMemo

执行 性能优化的最终检查清单:

  1. 确认 子组件是纯展示组件或计算开销较大。
  2. 使用 React.memo 包裹 子组件。
  3. 检查 父组件传递的 props,特别是函数类型。
  4. 对所有传递给子组件的函数,使用 useCallback 进行包裹
  5. 检查 useCallback 的依赖数组,确保只在必要时更新函数引用。
  6. 如果传递了对象或数组,使用 useMemo 进行包裹

评论 (0)

暂无评论,快来抢沙发吧!

扫一扫,手机查看

扫描上方二维码,在手机上查看本文