【翻译】React 重渲染机制解析

9 阅读4分钟

原文链接:React Re-Renders

作者:Serhii Shramko

你好,react Andy

核心问题:所有组件被无差别重渲染

我使用 React 已有六年多时间,发现即便是资深开发者,也常常难以理解 React 的底层运行机制,这很容易导致代码性能低下,进而影响代码质量、运行速度和用户体验。

React 官方文档非常适合入门学习,市面上也有大量面向初学者的书籍、课程和博客,但入门之后该如何深入?怎样才能真正理解其底层原理?如果你的 React 开发经验已有一定积累,基础甚至中级课程早已无法满足需求,而高阶的学习资源却十分稀缺。

这也是我推出本系列文章的初衷,希望能填补这一知识空白。

重渲染简介

理解 React 的重渲染机制,是实现应用性能优化的关键。你需要清楚重渲染的触发条件重渲染在应用中的传播方式,以及重渲染的执行过程和实际影响

实际开发中的问题

想象一下这样的场景:你在一家大厂实习,接到的第一个任务是开发一个 React 组件,需要在仪表盘顶部添加一个简单的按钮,点击后能打开设置面板。

初始的仪表盘组件代码如下:

const Dashboard = () => {
  // 此处为原有业务代码
  return <div className="container">
    {/* 按钮需要放在这个区域 */}
    <ExpensiveDataGrid />
    <AnalyticsWidget />
    <ActivityFeed />
  </div>;
};

接下来你开始实现需求,这个任务看起来十分简单,相信大家都做过无数次类似的开发:

const Dashboard = () => {
  // 新增状态管理
  const [isExpanded, setIsExpanded] = useState(false);

  // 状态更新时,此处返回的所有内容都会被重渲染
  return <div>
    {/* 新增触发按钮 */}
    <IconButton onClick={() => setIsExpanded(true)}>
      显示设置面板
    </IconButton>

    {/* 新增设置面板 */}
    {isExpanded && <SettingsPanel onDismiss={() => setIsExpanded(false)} />}

    <ExpensiveDataGrid />
    <AnalyticsWidget />
    <ActivityFeed />
  </div>;
};

你添加了状态来跟踪设置面板的展开 / 收起状态,新增了触发状态变更的按钮,并根据状态来控制面板的渲染。

但运行应用后你发现,点击按钮后,设置面板要等将近一秒钟才会显示,延迟感非常明显。

有经验的 React 开发者可能会立刻给出建议:

你这是让整个应用都重渲染了,给所有组件包一层 React.memo,再用 useCallback 缓存方法,就能避免不必要的重渲染。

这个建议并非毫无道理,但在动手优化前,我们不妨先停下来思考 —— 这个场景下,缓存优化其实并非必需,甚至盲目使用还可能影响性能。

首先,我们来仔细分析一下问题的本质,以及延迟产生的原因。

当点击按钮时,会触发setIsExpanded状态更新函数,将isExpandedfalse改为true。而持有这个状态的Dashboard组件,会因此触发自身的重渲染。

在状态更新、组件完成重渲染后,React 需要将新的数据传递给所有依赖的子组件,会自动重渲染该组件渲染出的所有子组件,并沿着组件树自上而下持续传播,直到叶子节点。

如果把一个典型的 React 应用看作树形结构,你会发现:从状态更新的触发节点开始,其下方的所有组件都会被重渲染

理解重渲染机制是掌握 React 的关键。

这是理解 React 重渲染的关键,需要牢记的一点是:React 永远不会向上触发重渲染。即便状态更新发生在组件树的中间节点,也只有该节点下方的组件会被重渲染。

当组件被React.memo包裹后,React 会中断默认的重渲染流程,先判断组件的属性是否发生变化。如果属性没有任何变更,就会终止重渲染;但只要有任意一个属性被修改,重渲染就会照常执行。

需要注意的是,通过缓存机制有效阻止重渲染是一个复杂的话题,涉及诸多考量因素。想要深入理解的话,建议大家继续学习相关内容(新文章即将推出,也可先阅读元素、作为属性的 Children 与重渲染一文)。

在某些场景下,用React.memo包裹组件确实能避免不必要的重渲染,但这种方式本身也存在不少复杂的细节和使用陷阱,这部分内容会在后续文章中探讨。更高效的解决方案是:将依赖特定状态的组件进行隔离,把状态和相关组件封装到一个独立的小型组件中。这种方式不仅能提升性能,还能让组件结构更清晰。

状态下推解决方案

接下来,我们将设置面板的相关逻辑抽离为独立组件:

const SettingsToggle = () => {
  const [isExpanded, setIsExpanded] = useState(false);

  return <>
    <IconButton onClick={() => setIsExpanded(true)}>
      显示设置面板
    </IconButton>

    {isExpanded && <SettingsPanel onDismiss={() => setIsExpanded(false)} />}
  </>;
};

然后在原有的Dashboard组件中引入这个新组件即可:

const Dashboard = () => {
  return <div>
    {/* 引入新封装的组件 */}
    <SettingsToggle />

    <ExpensiveDataGrid />
    <AnalyticsWidget />
    <ActivityFeed />
  </div>;
};

经过这样的改造后,点击按钮,设置面板能瞬间弹出。我们仅通过一种简单的组件组合技巧,就解决了这个严重的性能问题!