为什么当父组件改变时,React会重新渲染子组件?

23
如果父组件在React中重新渲染,无论传递的props是否更改,子组件也会重新渲染。
为什么React这样做?如果父组件重新渲染时不重新渲染没有更改的props的子组件会有什么问题?
更新:我正在React Devtools性能分析器中谈论这个问题: enter image description here 示例代码:
App.tsx:
 import React, { useMemo, useState } from "react";
    import "./App.css";
    import { Item, MyList } from "./MyList";
    
    function App() {
      console.log("render App (=render parent)");
    
      const [val, setVal] = useState(true);
      const initialList = useMemo(() => [{ id: 1, text: "hello world" }], []); // leads to "The parent component rendered"
      //const initialList: Item[] = []; // leads to "Props changed: (initialList)"
    
      return (
        <div style={{ border: "10px solid red" }}>
          <button type="button" onClick={() => setVal(!val)}>
            re-render parent
          </button>
          <MyList initialList={initialList} />
        </div>
      );
    }
    
    export default App;

MyList.tsx:

import { FC, useState } from "react";

export interface Item {
  id: number;
  text: string;
}

interface Props {
  initialList: Item[];
  //onItemsChanged: (newItems: Item[]) => void;
}

export const MyList: FC<Props> = ({
  initialList,
  //onItemsChanged,
}) => {
  console.log("render MyList");

  const [items, setItems] = useState(initialList);

  return (
    <div style={{ border: "5px solid blue" }}>
      <ul>
        {items.map((item) => (
          <li key={item.id}>{item.text}</li>
        ))}
      </ul>
      <button type="button">add list item (to be implemented)</button>
    </div>
  );
};

孩子们不会重新渲染是不正确的。 - Aaron Brager
@AaronBrager 我添加了React Devtools的屏幕截图和一些示例代码。它可能不会重新渲染DOM,但至少在React中会重新渲染,我想知道为什么会这样。 - stefan.at.kotlin
将整个“function”组件视为“class”组件的“render”方法。在那里放置“console.log”会使整个函数重新渲染。 - Hagai Harari
从文档中我了解到,即使你礼貌地要求他们不要这样做,人们往往会改变状态。因此,他们不是比较props,而是再次运行render并查看DOM是否需要更新。他们添加了PureComponent,它对于像您这样的情况进行属性的浅比较。(我试图表达的观点是,Props可能是复杂对象,比较成本高昂) - Nadia Chibrikova
@HagaiHarari 能否详细说明一下?我是React的新手,不熟悉类组件。然而,在我的Hooks版本中,我注释掉了console.log语句,但正如预期的那样,这并没有改变任何东西。 - stefan.at.kotlin
6个回答

14
React通过在每次状态改变(使用setState)或从props的变化中重新渲染组件,然后使用React的reconciliation算法将先前的呈现与当前呈现输出进行比较,以确定是否应该使用新更新将更改提交到组件树(例如DOM)中,从而实现快速响应的UI。
然而,不必要的组件重新呈现会发生并且可能很昂贵,在我参与的每个React项目中都是常见的性能陷阱。SOURCE 针对这个问题的解决方案是: 即使其props没有改变,组件也可以重新渲染。往往情况下,这是由于父组件重新呈现导致子组件重新呈现。 为了避免这种情况,我们可以使用React.memo()来包装子组件,以确保只有在props更改时才重新呈现:
function SubComponent({ text }) {
  return (
    <div>
      SubComponent: { text }
    </div>
  );
}
const MemoizedSubComponent = React.memo(SubComponent);

源代码


11

记忆化会生成与缓存相关的额外成本,这就是为什么当属性引用相同时React重新渲染组件的原因,除非您选择使用React.memo进行记忆化。

例如,如果考虑一个经常使用不同属性重新渲染的组件,并且如果记忆化是内部实现的细节,则React在每次重新渲染时必须执行两项任务:

  • 检查旧属性和当前属性是否引用相同。
  • 因为属性比较几乎总是返回false,所以React将执行以前和当前渲染结果的差异计算。

这意味着您可能会得到更差的性能。


这里是 React.PureComponentReact.memo 的文档,以及一篇关于 React.memo 的优秀文章 《使用 React.memo 控制函数组件的渲染》 - benhatsor
我不理解这里的推理,React 在每次重新渲染时都会执行 diffing 以决定是否更改 DOM。因此,我认为这不是问题的原因。我猜测 React 重新渲染是因为它“需要”。在 React 中重新渲染实际上只是重新运行一个函数,但没有运行此函数,React 就没有新的渲染结果与先前的结果进行比较,因此这根本不是性能优化,而是其重要组成部分。(尽管我可能错了,这只是我的“猜测”)。 - aderchox

2

使用React.memo包装你的组件,它将不会重新渲染

export const MyList: FC<Props> = React.memo(({
  initialList,
  //onItemsChanged,
}) => {
  console.log("render MyList");

  const [items, setItems] = useState(initialList);

  return (
    <div style={{ border: "5px solid blue" }}>
      <ul>
        {items.map((item) => (
          <li key={item.id}>{item.text}</li>
        ))}
      </ul>
      <button type="button">add list item (to be implemented)</button>
    </div>
  );
})

如果你想要理由,请参考这篇文章


谢谢,但我的问题不是如何修复重新渲染,而是要理解为什么React会这样做(如果父组件更改,则默认重新渲染子组件)。 - stefan.at.kotlin
这是答案的一部分,当您更改状态时,父组件应该触发重新渲染视图,除了由React Memo保护的子组件。 - ARZMI Imad
但这是为什么呢?如果属性没有改变,为什么要重新渲染子组件?如果React默认执行memo之类的操作会有哪些问题? - stefan.at.kotlin
你可能正在查看这个链接 https://dev59.com/V1QJ5IYBdhLWcg3wzJIP#63405621 - ARZMI Imad
谢谢,那个链接很棒。经过一些实验,我们发现使用memo进行初始渲染(该渲染还包括useMemo,useCallback等,以使props保持稳定)会略微增加渲染时间,但任何进一步的(被预防的)渲染甚至能提高简单组件的性能。 - stefan.at.kotlin
欢迎 @stefan.at.wpf - ARZMI Imad

1
组件只有在shouldComponentUpdate()返回true时才会重新渲染,默认情况下是返回true的。通常情况下这不会造成太大问题,因为如果没有变化,DOM仍然不会更新。作为一种优化,您可以添加逻辑来防止子组件重新渲染,如果它的状态和属性没有改变,如下所示:
shouldComponentUpdate(nextProps, nextState) {
    return (this.props.someProp !== nextProps.someProp && this.state.someState !== nextState.someState);
}

请记住,这只是一种性能优化方式,如果不确定,请坚持使用默认行为。这里是官方文档的链接。

1

这是一个小比喻,应该会有所帮助

假设你有一个盒子,里面还有一个盒子。如果你想用一个新盒子替换外面的盒子,那么你必须同时替换里面的盒子。

你的父组件就像外面的盒子,子组件就像里面的盒子。如果你清除第一次渲染以为新渲染腾出空间,那么新的渲染将在里面重新渲染新的子组件。

我希望这可以帮助你理清事情。

编辑:

如果你不重新渲染父组件中的子组件,那么可能传递给子组件的任何 props 都不会在其中更新,因此,子组件将永远没有动态 props,并且 ReactJS 的整个目的也将丧失。如果是这种情况,你根本不会来这里问这个问题。


1
这个比喻只在引用卸载父级组件时才成立,但是为什么父组件的属性改变后就一定要重新渲染子组件呢? - Mordechai
这个类比是指重新渲染父组件。如果父组件的 props 发生改变,那么父组件当然会重新渲染,这意味着它所有的子组件也必须重新渲染,因为界面已经被刷新。 - GoodStuffing123

1

当父组件渲染时,如果使用了memouseCallback,则不会重新渲染子组件。

子组件:

使用memo将导致React跳过渲染组件,如果其props没有改变。

import { React, memo } from "react";
import { Typography, TextField } from "@mui/material";

function PasswordComponent(props) {
  console.log("PasswordComponenmt");
  return (
    <div>
      <Typography style={{ fontWeight: "900", fontSize: 16 }}>
        Password
      </Typography>
      <TextField
        size="small"
        type="password"
        variant="filled"
        value={props.password}
        onChange={(e) => {
          props.onChangePassword(e.target.value);
        }}
      />
    </div>
  );
}
export default memo(PasswordComponent);

父组件:

React的useCallback Hook返回一个记忆化的回调函数。 只有当其依赖项之一更新时,useCallback Hook才会运行。 这可以提高性能。

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

export default function ParentComponent() {
  const [password, setpassword] = useState("");

  const onChangePassword = useCallback((value) => {
    setpassword(value);
  }, []);

  return <PasswordComponent onChangePassword={onChangePassword} />;
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接