React父子组件重渲染异常刷新问题

发布于:2025-04-08 ⋅ 阅读:(33) ⋅ 点赞:(0)

问题描述

在进行项目前端开发的过程中,遇到的一个数据刷新问题。

前端使用的是React框架,Antd组件库。

1.png

在上述这个页面中,弹窗的是子组件,背景页面是父组件,子组件的操作导致了父组件和子组件一同进行了重渲染。

实际展示出的效果就是点击了加分按钮,这个小弹窗被瞬间关闭再打开,这很显然不是期望的效果。

问题分析

为了查明这个问题,我去查询了相关资料,搞明白了React底层的Hook函数的重渲染机制。

首先,如果只有一个组件,当组件内部的useState创建的值发生了改变(setState),会导致React发生重渲染,但它只会定向的渲染改变了的部分,这是由于React的Reconciliation算法。

Reconciliation 被称为 diff 算法,它用来比较两颗 React 元素树之间的差异,为了让组件重新渲染变得高效,React 尽可能地复用现有的组件和 DOM。为了降低时间复杂度,Diff 算法基于如下两个假设:

  • 两个不同类型的元素对应的元素树完全不同。
  • 在同一个列表中,如果两个元素key属性的值相同,那么它们被识别为同一个元素。

但当这一组件内部有子组件时,父组件无法知晓子组件内部哪些改变了,哪些没有改变,所以React 的重渲染会直接渲染它所有的子组件,不关心子组件是否改变了,它会无条件地渲染子组件。

这一机制在一般情况下并不会引发什么问题,但在我的上述场景下让用户的使用体验很糟糕。我上述问题中,弹窗作为子组件,使用了父组件传过来的useState创建的数据,并且在点击加分按钮时,改变了这一数据,进行了setState,这就导致了父组件刷新,整个弹窗子组件也全部重渲染,使得弹窗“闪现”了一下。

解决方案

因为父组件的更新导致了子组件树全部重渲染,因此让子组件跳过重渲染就可以了。

React 提供了 3 个主要的API让我们跳过重新渲染:

  • React.Component 的 shouldComponentUpdate:这是类组件可选的生命周期函数,它在组件 render 阶段早期被调用,如果返回false,React 将跳过重新渲染该组件,使用它最常见的场景是检查组件的 props 和 state 是否自上次以来发生了变更,如果没有改变则返回false。
  • React.PureComponent:它在 React.Component 的基础上添加默认的 shouldComponentUpdate 去比较组件的 props 和 state 自上次渲染以来是否有变更。
  • React.memo():它是一个高阶组件,接收自定义组件作为参数,返回一个被包裹的组件,被包裹的组件的默认行为是检查 props 是否有更改,如果没有,则跳过重新渲染。

上述方法都通过‘浅比较’来确定值是否有变更,如果通过 mutable 的方式修改状态,这些 API 会认为状态没有变。

  • 如果组件在其渲染过程中返回的元素的引用与上一次渲染时的引用完全相同,那么 React 不会重新渲染引用相同的组件。示例如下:
function ShowChildren(props: {children: React.ReactNode}) {
    const [count, setCount] = useState<number>(0)
    return (
        <div>
            {count} <button onClick={() => setCount(c => c + 1)}>click</button>
            {/* 写法一 */}
            {props.children}
            {/* 写法二 */}
            {/* <Children/> */}
        </div>
    )
}

上述 ShowChildren 的 props.children 对应 Children 组件,因此写法一和写法二在浏览器中呈现一样。点击按钮不会让写法一的 Children 组件重新渲染,但是会使写法二的 Children 组件重新渲染。

上述4种方式跳过重新渲染意味着 React 会跳过整个子树的重新渲染。

当然,还有一种简单粗暴的解决办法,就是把父子组件全部拆分放到一起,都变成兄弟组件👍,简单高效,就是以后重新看这段代码可能会想打人。