在你的组件显示在屏幕上之前,它们必须由 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.
    提交

步骤 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.
    ……渲染!

步骤 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,浏览器将重新绘制屏幕。虽然此过程被称为“浏览器渲染”,但为了避免在文档中造成混淆,我们将将其称为“绘制”。

回顾

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