在组件显示在屏幕上之前,它们必须由 React 进行渲染。了解此过程中的步骤将帮助您思考代码的执行方式并解释其行为。

您将学习

  • 渲染在 React 中的含义
  • React 何时以及为何渲染组件
  • 在屏幕上显示组件所涉及的步骤
  • 为何渲染并不总是产生 DOM 更新

假设您的组件是厨房里的厨师,他们用食材烹饪美味佳肴。在这种情况下,React 就是服务员,接收顾客的点餐并为他们上菜。此 UI 请求和服务过程分为三个步骤:

  1. 触发渲染(将顾客的订单送到厨房)
  2. 渲染组件(在厨房准备订单)
  3. 提交到 DOM(将订单放在餐桌上)
  1. React as a server in a restaurant, fetching orders from the users and delivering them to the Component Kitchen.
    触发
  2. The Card Chef gives React a fresh Card component.
    渲染
  3. React delivers the Card to the user at their table.
    提交

插图作者 Rachel Lee Nabors

步骤 1:触发渲染

组件渲染的原因有两个:

  1. 这是组件的初始渲染
  2. 组件(或其某个祖先组件)的状态已更新

初始渲染

当您的应用启动时,您需要触发初始渲染。框架和沙盒有时会隐藏此代码,但它是通过使用目标 DOM 节点调用 createRoot,然后使用您的组件调用其 render 方法来完成的。

import Image from './Image.js';
import { createRoot } from 'react-dom/client';

const root = createRoot(document.getElementById('root'))
root.render(<Image />);

尝试注释掉 root.render() 调用,看看组件是否消失了!

状态更新时重新渲染

初始渲染组件后,您可以通过使用 set 函数更新其状态来触发进一步的渲染。更新组件的状态会自动将其排入渲染队列。(您可以将这些想象成餐厅顾客在下第一个订单后,根据他们的口渴或饥饿程度,再点茶、甜点和各种东西。)

  1. React as a server in a restaurant, serving a Card UI to the user, represented as a patron with a cursor for their head. They patron expresses they want a pink card, not a black one!
    状态更新...
  2. React returns to the Component Kitchen and tells the Card Chef they need a pink Card.
    ...触发...
  3. The Card Chef gives React the pink Card.
    ...渲染!

插图作者 Rachel Lee Nabors

步骤 2:React 渲染您的组件

在您触发渲染后,React 会调用您的组件以确定要在屏幕上显示的内容。“渲染”就是 React 调用您的组件。

  • 在初始渲染时,React 将调用根组件。
  • 对于后续渲染,React 将调用状态更新触发渲染的函数组件。

此过程是递归的:如果更新后的组件返回了其他组件,React 将接下来渲染*该*组件,如果该组件也返回了其他组件,它将接下来渲染*该*组件,依此类推。该过程将一直持续到没有更多嵌套组件并且 React 精确知道应该在屏幕上显示什么为止。

在以下示例中,React 将多次调用 Gallery()Image()

export default function Gallery() {
  return (
    <section>
      <h1>Inspiring Sculptures</h1>
      <Image />
      <Image />
      <Image />
    </section>
  );
}

function Image() {
  return (
    <img
      src="https://i.imgur.com/ZF6s192.jpg"
      alt="'Floralis Genérica' by Eduardo Catalano: a gigantic metallic flower sculpture with reflective petals"
    />
  );
}

  • 在初始渲染期间,React 将会 创建 DOM 节点 用于 <section><h1> 以及三个 <img> 标签。
  • 在重新渲染期间,React 将会计算自上次渲染以来,它们的哪些属性(如果有)发生了变化。它不会对这些信息进行任何处理,直到下一步,即提交阶段。

陷阱

渲染必须始终是 纯计算

  • 相同的输入,相同的输出。 给定相同的输入,组件应该始终返回相同的 JSX。(当有人点了一份带西红柿的沙拉时,他们不应该收到一份带洋葱的沙拉!)
  • 它只关心自己的事情。 它不应该改变渲染之前存在的任何对象或变量。(一个订单不应该改变其他任何人的订单。)

否则,随着代码库复杂性的增加,您可能会遇到令人困惑的错误和不可预测的行为。在“严格模式”下开发时,React 会调用每个组件的函数两次,这有助于发现由不纯函数引起的错误。

深入探讨

优化性能

如果更新的组件在树中非常高,则渲染该组件中嵌套的所有组件的默认行为并不是性能最佳的。如果您遇到性能问题,“性能” 部分中描述了几种可选择的解决方法。不要过早优化!

步骤 3:React 将更改提交到 DOM

渲染(调用)您的组件后,React 将会修改 DOM。

  • 对于初始渲染,React 将会使用 appendChild() DOM API 将其创建的所有 DOM 节点放到屏幕上。
  • 对于重新渲染,React 将会应用最少必要的操作(在渲染时计算!)来使 DOM 与最新的渲染输出相匹配。

仅当渲染之间存在差异时,React 才会更改 DOM 节点。 例如,这里有一个组件,它每秒都会重新渲染,并从其父组件传递不同的 props。请注意,您可以如何在 <input> 中添加一些文本,更新其 value,但组件重新渲染时,文本不会消失

export default function Clock({ time }) {
  return (
    <>
      <h1>{time}</h1>
      <input />
    </>
  );
}

这是有效的,因为在最后一步中,React 仅使用新的 time 更新了 <h1> 的内容。它看到 <input> 出现在与上次相同位置的 JSX 中,因此 React 不会触碰 <input> 或它的 value

结语:浏览器绘制

在渲染完成后,React 更新了 DOM,浏览器将重新绘制屏幕。尽管此过程被称为“浏览器渲染”,但为了避免在整个文档中造成混淆,我们将其称为“绘制”。

A browser painting 'still life with card element'.

插图作者 Rachel Lee Nabors

总结

  • React 应用程序中的任何屏幕更新都通过三个步骤完成
    1. 触发
    2. 渲染
    3. 提交
  • 您可以使用严格模式来查找组件中的错误
  • 如果渲染结果与上次相同,React 不会触碰 DOM