当前位置: 首页 > article >正文

React中每次渲染都会传入一个新的props.children到子组件?

传入props.children后, 为什么会导致组件的重新渲染?

问题描述

在 react 中, 我想要对组件的渲染进行优化, 遇到了一个非常意思的问题, 当我向一个组件中传入了 props.children 之后, 每次父组件重新渲染都会导致这个组件的重新渲染; 它看起来的表现就像是被memo包裹的组件, props和自身状态未发生变化, 组件却重新渲染了; 下面我写了一个demo, 一起来看看这个问题吧:

父组件App中引入了一个Home组件:

import Home from "./pages/Home";
import { useState } from "react";

function App() {
  const [count, setCount] = useState(0);
  console.log("App is render");

  return (
    <div className="App">
      {count}
      <button onClick={() => setCount(count + 1)}>Increment</button>
      <Home></Home>
    </div>
  );
}

使用 memo 包裹 Home 子组件, 同时 Home 组件可以接收一个 props.children 展示传入到 Home 中的组件, 如下:

import React, { memo } from "react";

const Home = memo((props) => {
  console.log("Home is render");
  return (
    <div>
      Home
      {props.children}
    </div>
  );
});

export default Home;

目前在 App 组件中, 没有向 Home 组件中传入 props.children, 此时第一次加载时 App 组件和 Home 组件都会重新渲染, 当我们点击 Increment 按钮让 count 的值变化时, App 组件重新渲染, 由于 Home 组件被 memo 包裹, 当 Home 组件的 props 和自身状态未发生变化时, 组件不进行重新渲染, 目前也正是我们所期望的这样, 没有问题。

但是, 当我们在 App 组件中向 Home 组件传入 props.children 时, 就会出现问题(此问题不仅限于我下面例子中传入了一个 About 组件, 传入任何元素都会出现这个问题, 即使我们传入一个简单的 div 元素):

import { useState } from "react";
import Home from "./pages/Home";
import About from "./pages/About";

function App() {
  const [count, setCount] = useState(0);
  console.log("App is render");

  return (
    <div className="App">
      {count}
      <button onClick={() => setCount(count + 1)}>Increment</button>
      <Home>
        <About />
      </Home>
    </div>
  );
}

About 组件同样使用 memo 包裹, 代码如下:

import React, { memo } from "react";

const About = memo(() => {
  console.log("About is render");
  return <div>About</div>;
});

export default About;

此时如果我们修改 count 的值, 会导致 App 组件重新渲染, 但是也会导致 Home 组件重新渲染。这就有些令人疑惑, 我们来分析一下:

首先我们知道, 在未经过任何优化的情况下, 父组件重新渲染一定会导致子组件的重新渲染, 那么也就会创建一个新的组件实例; 而如果使用 memo 对组件进行包裹, 那么在组件的 props 和自身状态没有发生变化的情况下, 父组件重新渲染子组件不会重新渲染, 是不是意味着不会创建一个新的组件实例呢? (这里进入了思维误区)

上面代码中, 我们向 Home 组件中传递了一个 About 组件, 目前 Home 组件中的表现就相当于 props.children = <About/>, 由于 Home 组件被 memo 包裹还重新渲染了, 那大几率是 props 发生了变化。纠结之处就在于, 此时 props 中又只有 children 一个属性, 值为 About 组件, About 组件同样被 memo 包裹, 且没有依赖任何 props 和状态, 如果 About 组件返回的结果应该是相同的, 就不应该导致 Home 组件的 props 发生变化才对。

这就是我所遇到的问题, 为什么 props.children 会影响组件的渲染呢?

问题分析

我依然怀疑是由 Home 组件的 props 发生了变化, 唯一可能变化的就是 About 组件, 为了验证我的想法, 于是我在Home 组件中定义了一个 aboutRef 变量, 使用 useRef 包裹 About 组件, 如下所示:

import Home from "./pages/Home";
import { useState } from "react";

function App() {
  const [count, setCount] = useState(0);
  // 使用useRef包裹
  const aboutRef = useRef(<About/>);
  console.log("App is render");

  return (
    <div className="App">
      {count}
      <button onClick={() => setCount(count + 1)}>Increment</button>
      <Home>{aboutRef.current}</Home>
    </div>
  );
}

此时我发现, 首次渲染时 App、Home、About 都会渲染, 而当 count 发生变化时, 只有 App 组件重新渲染了, 这也就达到了我最初期望的效果。但是为什么包裹了 useRef 才可以做到这个效果呢? 到这里已经可以确定的是 Home 组件的 props.children 一定是发生了变化的, 那么我们来探讨一下 About 组件为什么会变化。

变化的原因是因为组件每次重新渲染时都会创建 React 元素, 例如<About /> = jsx(About), 并且在调用时会返回一个新对象, 当然不只是 About 会这样创建, 其他组件和元素也是这样创建的。其中jsx()只不过是React.createElement 的语法糖而已, 元素或组件都会通过 React.createElement 创建返回一个 ReactElement 对象, 这是因为 React 利用 ReactElement 对象组成了一个 Javascript 对象树(也就是虚拟 DOM )。前面我进入了一个思维误区, 认为 memo 包裹的组件不会再被重新创建了, 其实不管是否有memo包裹, 都是会通过 React.createElement 来创建, 只不过被memo包裹的组件创建出来的 React 元素会有所不同, 具体的可以深入的学习 memo, 这里给大家推荐一篇文章《从源码学 API 系列之 React.memo》。

因此对于 props.children 而言, 每次得到的都是 React.createElement(About) 返回的一个新对象, 这也是 Home 组件的 props 改变了的原因; 而我们使用 useRef, 创建了一个不会改变的对象赋值给 Home 组件的 props, 所以 Home 组件的 props 没有发生变化, 就不会重新渲染。

解决方案

解决这个问题, 除了使用 useRef 之外, 我们还可以定义一个变量, 提到 App 组件外, 也可以做到这个效果, 如下所示:

import { useState } from "react";
import Home from "./pages/Home";
import About from "./pages/About";

// 在组件外定义变量
const about = <About />;

function App() {
  const [count, setCount] = useState(0);
  console.log("App is render");

  return (
    <div className="App">
      {count}
      <button onClick={() => setCount(count + 1)}>Increment</button>
      <Home>{about}</Home>
    </div>
  );
}

当 About 组件没有依赖于 App 组件中其他状态时, 我们可以采用上面的做法, 但是如果 About 组件还依赖 App 内的其他状态, 可以发现无论是提变量还是 useRef 的做法都无法实现, 例如 About 组件中接收一个 name 参数, 由 App 组件传入:

import React, { memo } from "react";

// 接收一个props.name
const About = memo(({ name }) => {
  console.log("About is render");
  return <div>About: {name}</div>;
});

export default About;

这个时候我们就需要借助于 useMemo 进行优化(不用 useCallback 的原因是 useCallback 作用于函数, useMemo 作用于返回值, 在这里很明显我们想要作用于函数返回的组件), 就做到了实现当 count 发生变化时, 只有 App 组件重新渲染, 而 name 属性变化时 App、Home、About 都会重新渲染:

function App() {
  const [count, setCount] = useState(0);
  // 传入About组件的状态
  const [name, setName] = useState("Hello");
	// 使用useMemo优化
  const about = useMemo(() => <About name={name} />, [name]);

  console.log("App is render");

  return (
    <div className="App">
      {count}
      <button onClick={() => setCount(count + 1)}>Increment</button>
      <button onClick={() => setName("abc")}>Change Name</button>
      <Home>{about}</Home>
    </div>
  );
}

http://www.kler.cn/news/162144.html

相关文章:

  • Sentinel基础知识
  • WT588F02B单片机语音芯片在磁疗仪中的应用介绍
  • 【算法每日一练]-结构优化(保姆级教程 篇6 分块,倍增)#HDU4417超级马里奥 #poj2019玉米田 #POJ3368频繁值
  • Linux下的同步命令代码编写
  • 借助webpack来优化前端性能
  • Linux学习教程(第十一章 Linux高级文件系统管理)二
  • C语言第四十四弹---调整奇偶数顺序
  • 广州数字孪生赋能工业制造,加速推进制造业数字化转型
  • Spark---Spark on Hive
  • 利用proteus实现串口助手和arduino Mega 2560的串口通信
  • Linux 常用命令汇总
  • Java网络编程 *TCP与UDP协议*
  • 使用Caliper对Fabric地basic链码进行性能测试
  • 【私藏】国内最全的电商API数据接口分享各种业务场景调用API代理的API接口教程
  • 查看Linux的Ubuntu的版本
  • pytorch 模型量化quantization
  • JAVA后端自学技能实操合集
  • Qt之基于QCustomPlot绘制直方图(Histogram),叠加正态分布曲线
  • vmware安装centos7总结
  • VSCODE 运行C程序缓慢解决方法之一
  • Ubuntu22.04安装Mariadb
  • C语言printf的输出格式大全及颜色字体打印
  • 微信小程序中block和View组件的使用区别
  • AI发展下服务器的选择非常重要
  • mysql 链接超时的几个参数详解
  • 嵌入式总线技术学习(文章链接汇总)
  • C语言——指针(五)
  • 【C/PTA —— 15.结构体2(课内实践)】
  • 3D材质编辑:制作被火烧的木头
  • ERP数据仓库模型