新闻中心

React 重新渲染深度解析:为何 children 组件会被重复渲染及优化策略

2025-10-06
浏览次数:
返回列表

react 重新渲染深度解析:为何 children 组件会被重复渲染及优化策略

本文深入探讨了 React 组件在父组件状态更新时,即使通过 children prop 传递,子组件仍可能被重复渲染的常见问题。核心原因在于父组件每次渲染时,若子组件在 JSX 中被内联声明,React 会创建新的子组件实例。文章通过具体代码示例,详细解释了这一机制,并提供了将状态管理下移以稳定 children prop 的优化方案,旨在帮助开发者更好地理解和优化 React 应用的渲染性能。

理解 React 组件的重新渲染机制

在 React 应用开发中,组件的重新渲染(re-render)是一个核心概念,但其行为有时会超出开发者的预期。一个常见的误解是,如果一个父组件的 children prop 逻辑上没有改变,那么传递给它的子组件就不会重新渲染。然而,当父组件自身因状态更新而重新渲染时,如果子组件是在父组件的 JSX 渲染逻辑中内联声明的,即使其内容看似不变,React 也会将其视为一个新的组件实例,从而触发子组件的重新渲染。

这背后的机制是:每当一个组件函数执行时(即发生渲染),如果其 JSX 中包含对另一个组件的引用,React 会为该引用创建一个新的 React 元素(React Element)。即使这个新的 React 元素在类型和 props 上与上一次渲染的元素完全相同,但由于它是一个“新”的对象实例,React 在协调(reconciliation)过程中会认为这个子组件可能需要更新,并会访问其子树进行比对,最终导致子组件函数被再次调用,即重新渲染。

问题示例:children 组件的意外重新渲染

考虑以下 React 应用结构,其中 App 组件包含一个定时器,每 100 毫秒更新一次自身状态,进而导致 App 组件重新渲染。App 将 Child 组件作为 Parent 组件的 children prop 传递。

import { useState, useEffect } from 'react';

// Child 组件,每次渲染都会在控制台输出 'rendered'
const Child = () => {
  console.log('Child rendered');
  return (
    <p>这是一个子组件内容。</p>
                    <div class="aritcle_card">
                        <a class="aritcle_card_img" href="/ai/1540">
                            <img src="https://img.php.cn/upload/ai_manual/000/969/633/68b7a213277e9355.png" alt="AI Surge Cloud">
                        </a>
                        <div class="aritcle_card_info">
                            <a href="/ai/1540">AI Surge Cloud</a>
                            <p>低代码数据分析平台,帮助企业快速交付深度数据</p>
                            <div class="">
                                <img src="/static/images/card_xiazai.png" alt="AI Surge Cloud">
                                <span>87</span>
                            </div>
                        </div>
                        <a href="/ai/1540" class="aritcle_card_btn">
                            <span>查看详情</span>
                            <img src="/static/images/cardxiayige-3.png" alt="AI Surge Cloud">
                        </a>
                    </div>
                
  );
}

// Parent 组件,接收并渲染 children prop
const Parent = ({ children }) => {
  return (
    <div id='parent'>
      {children}
    </div>
  );
}

// App 组件,包含一个定时器更新自身状态
const App = () => {
  const [now, setNow] = useState();

  // 启动一个定时器,每100ms更新 'now' 状态
  useEffect(() => {
    const interval = setInterval(() =>
      setNow(Date.now()), 100);
    return () => clearInterval(interval); // 清理定时器
  }, []);

  return (
    <div className="App">
      <Parent>
        {/* Child 组件在这里被内联声明 */}
        <Child />
      </Parent>
    </div>
  );
}

export default App;

运行上述代码,你会发现控制台每 100 毫秒都会输出 Child rendered。尽管 Child 组件没有任何自身状态或接收任何 props,并且 Parent 组件也只是简单地渲染其 children prop,Child 依然在 App 组件状态更新时被重复渲染。

原因分析:

当 App 组件的状态 now 更新时,App 组件会重新执行其渲染函数。在每次执行时,App 组件的 JSX 表达式 都会被重新评估。这意味着,每次 App 渲染时,都会创建一个新的 React 元素实例。

尽管这个新的 元素与上一次渲染的 元素在类型和结构上是相同的,但对 React 而言,它是一个全新的 J*aScript 对象引用。因此,当 App 将这个新的 元素作为 Parent 的 children prop 传递时,React 会认为 Parent 组件的 children prop 已经改变(因为它是一个新的对象引用),从而触发 Parent 和其 children(即 Child 组件)的重新渲染。

优化方案:稳定化 children Prop

为了避免 Child 组件在 App 组件状态更新时进行不必要的重新渲染,我们需要确保传递给 Parent 组件的 children prop 在 App 重新渲染时保持引用稳定。最直接有效的方法是将导致重新渲染的状态或副作用下移到组件树中更低层级的组件,使其不会影响到不相关的上层组件或兄弟组件。

在我们的例子中,App 组件中的定时器状态更新是导致 Child 重新渲染的根本原因。如果我们将这个定时器逻辑移动到 Parent 组件内部,那么 App 组件将不再因定时器而重新渲染,从而稳定了传递给 Parent 的 children prop。

import { useState, useEffect } from 'react';

const Child = () => {
  console.log('Child rendered');
  return (
    <p>这是一个子组件内容。</p>
  );
}

// Parent 组件,现在包含了定时器状态
const Parent = ({ children }) => {
  const [now, setNow] = useState(); // 状态移至 Parent

  // 启动一个定时器,每100ms更新 'now' 状态
  useEffect(() => {
    const interval = setInterval(() =>
      setNow(Date.now()), 100);
    return () => clearInterval(interval);
  }, []);

  return (
    <div id='parent'>
      {children}
    </div>
  );
}

// App 组件现在不包含定时器逻辑
const App = () => {
  return (
    <div className="App">
      <Parent>
        {/* Child 组件仍然在这里被内联声明 */}
        <Child />
      </Parent>
    </div>
  );
}

export default App;

通过将 useState 和 useEffect 钩子从 App 组件移动到 Parent 组件,App 组件在首次渲染后,其内部将不再有状态更新导致自身重新渲染。这意味着 这部分 JSX 表达式只会在 App 组件挂载时执行一次。因此,传递给 Parent 的 React 元素引用将保持稳定。

现在,Parent 组件会每 100 毫秒更新其内部的 now 状态并重新渲染。然而,由于 App 组件不再重新渲染,它传递给 Parent 的 children prop(即 元素)的引用始终是第一次创建的那个。当 Parent 重新渲染时,React 发现其 children prop 的引用没有改变,因此不会访问 Child 组件的子树,Child 组件也就不会重新渲染。此时,控制台将只输出一次 Child rendered。

注意事项与最佳实践

  1. 状态下移原则: 尽可能将状态和副作用放置在组件树中需要它们的最底层组件。这可以有效限制重新渲染的范围,避免不必要的性能开销。

  2. React.memo 的作用: 对于功能组件,可以使用 React.memo 来进行性能优化。React.memo 会对组件的 props 进行浅比较,如果 props 没有改变,则跳过组件的重新渲染。在上述示例中,即使 App 仍然包含定时器,如果 Child 组件被 React.memo 包裹,并且它不接收任何 props,它将不会重新渲染。但请注意,React.memo 仅在 props 引用稳定时才有效。如果像原始问题中那样,每次父组件渲染都创建一个新的 Child 元素,即使 Child 被 memo 包裹,它仍然会接收到一个“新”的 children prop 引用,从而导致重新渲染。

    const MemoizedChild = React.memo(() => {
      console.log('MemoizedChild rendered');
      return <p>这是一个被 memoized 的子组件内容。</p>;
    });
    
    // ... 在 App 中使用 <MemoizedChild />

    然而,在我们的原始问题场景中,Child 是作为 Parent 的 children prop 传递的。如果 Parent 被 React.memo 包裹,而 App 每次都传递一个新的 元素给 Parent 的 children prop,那么 Parent 的 children prop 引用仍然会改变,Parent 依然会重新渲染,并进而渲染其 children。因此,React.memo 主要用于优化组件自身因父组件 props 变化而导致的重新渲染,而不是解决父组件内联创建子组件实例的问题。

  3. 理解 JSX 的本质: JSX 语法糖最终会被编译成 React.createElement() 调用。每次父组件渲染时,JSX 中的组件标签都会被转换为新的 React.createElement() 调用,生成新的 React 元素对象。理解这一点有助于避免关于组件重新渲染的误解。

  4. 组件组合的权衡: “提升内容” (lifting content up) 是一种强大的组合模式,可以将子组件的渲染逻辑与父组件的业务逻辑解耦。但在使用时,需要注意其对渲染性能的影响,确保传递的 children prop 引用在不必要时是稳定的。

总结

React 的渲染机制是其高性能的基础,但如果不深入理解其工作原理,可能会遇到一些性能陷阱。当父组件重新渲染且在 JSX 中内联声明子组件时,React 会创建新的子组件实例,即使其逻辑内容不变,也可能导致不必要的重新渲染。通过将状态和副作用下移到更合适的组件层级,我们可以有效稳定传递给子组件的 children prop 引用,从而避免这些不必要的重新渲染,提升应用的性能和用户体验。

以上就是React 重新渲染深度解析:为何 children 组件会被重复渲染及优化策略的详细内容,更多请关注其它相关文章!


# 在这里  # 佛山陶瓷厂网站优化排名  # 亚马逊站内营销推广  # 黄贝企业站网站建设  # 昆明网站关键词优化费用  # 南京论坛营销推广技巧  # 短信营销推广宣传  # seo排名优化套餐  # 谷歌seo推广公司固安  # 苏州关键词排名招商  # 临渭网站推广公司电话  # 移到  # 自定义  # 它是  # 会在  # react  # 创建一个  # 是一个  # 使其  # 这是一个  # 子树  # red  # 组件渲染  # 常见问题  # 应用开发  # app  # js  # java  # javascript 


相关栏目: 【 科技资讯46185 】 【 网络学院92790


相关推荐: 解决macOS上安装pyhdf时‘hdf.h’文件缺失的编译错误  J*a应用程序首次运行自动创建文件与目录的最佳实践  QQ邮箱网页版入口登录 QQ邮箱在线邮箱官方通道  Angular响应式表单:实现提交后表单及按钮的禁用与只读化  如何使用Rector自动化升级旧代码_通过Composer安装和配置Rector进行代码重构  谷歌浏览器一键优化方案_谷歌浏览器直达主页极速不卡版  网易大神怎么保存别人动态的图片_网易大神动态图片保存方法  铃兰之剑为这和平的世界希里技能组及加点推荐  高德地图公交到站提醒失败如何解决 高德提醒权限设置  AO3最新可访问网址 Archive of Our Own官方在线入口  Node.js CSV 数据处理:基于字段空值条件过滤整条记录的策略  css卡片内容溢出如何处理_使用overflow隐藏或scroll显示内容  中兴BladeV30怎样用测距估书架层高_iPhone中兴BladeV30测距估书架层高【家装参考】  Go语言中Map值调用指针接收器方法的限制与应对  神庙逃亡小游戏在线玩 神庙逃亡小游戏入口  PDF怎么合并PDF并保持格式_PDF合并文件保持排版教程  如何在Promise链中优雅地中断后续then执行  Golang切片为何属于引用类型_Golang slice底层结构与引用语义说明  没有大陆身份证/银行卡如何实名微信? 亲测有效的几种方法分享  谷歌邮箱网页版官方页面入口 谷歌邮箱网页端快速访问  微信客户端如何收红包_微信客户端接收红包使用教程  厨房不锈钢水槽发黑生锈怎么处理_水槽用可乐+锡纸2分钟抛亮如新  照顾宝贝2小游戏免费秒玩入口  iCloud登录入口网页版 苹果iCloud官网登录  蛙漫安全无毒 官方认证的绿色入口  Win10如何清理注册表垃圾 Win10手动清理无效注册表【技巧】  css滚动区域卡顿如何改善_css滚动问题用will-change优化渲染  vivo浏览器怎么扫描二维码 vivo浏览器内置扫一扫功能使用方法  在Blazor WebAssembly应用中动态注入客户端特定指标代码的策略  如何在J*a中使用Locale处理多语言环境  HuggingFaceEmbeddings中向量嵌入维度调整的限制与理解  J*aScript中高效管理与清空动态列表:避免循环陷阱  C++如何实现单例模式_C++设计模式之线程安全的单例写法  J*aScript 字符串标签转换:使用正则表达式高效替换  美团外卖商家服务中心入口 美团商家版官网入口  Win11怎么开启高性能模式_Windows 11电源计划优化设置  机器学习中对数变换预测结果的反向还原  AO3最新镜像入口 Archive of Our Own官方平台访问  迅雷下载到U盘速度很慢怎么办_迅雷U盘下载慢优化方法  win11 arm版怎么安装 M1/M2 Mac虚拟机安装ARM win11的方法  如何创建独立于主系统的J*a运行环境_隔离式环境搭建策略  windows10怎么关闭系统提示音_windows10彻底静音设置方法  韩小圈电脑版在线入口_网页版免费登录地址  深入理解Go语言中Map值与方法接收器的交互:为什么需要临时变量  PyTorch模型训练准确率不提升:诊断与修复常见指标计算错误  支付宝解绑银行卡步骤_支付宝如何解除绑定银行卡  电脑IP地址怎么查 查看本机IP地址的几种方法  Go与Ruby之间实现AES加密互通:CFB模式下的密钥长度匹配策略  AO3最新入口2025公告_AO3中文官网合集  ArrayList与LinkedList核心操作的Big-O复杂度分析 

搜索