React渲染和更新机制及其核心内容详解

时间:2024-11-20 22:27:17

0.Overview

步骤 1: 触发一次渲染 

初次渲染 

当应用启动时,会触发初次渲染。它是通过调用目标 DOM 节点的 createRoot,然后用你的组件调用 render 函数完成的:

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

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

//Image.js
export default 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"
    />
  );
}
 状态更新时重新渲染 

     一旦组件被初次渲染,你就可以通过使用 set 函数 更新其状态来触发之后的渲染。更新组件的状态会自动将一次渲染送入队列

步骤 2: React 渲染你的组件 

  • 在进行初次渲染时, React 会调用根组件。
  • 对于后续的渲染, React 会调用内部状态更新触发了渲染的函数组件。

这个过程是递归的:如果更新后的组件会返回某个另外的组件,那么 React 接下来就会渲染 那个 组件,而如果那个组件又返回了某个组件,那么 React 接下来就会渲染 那个 组件,以此类推。这个过程会持续下去,直到没有更多的嵌套组件并且 React 确切知道哪些东西应该显示到屏幕上为止。

步骤 3: React 把更改提交到 DOM 上 

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

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

React 仅在渲染之间存在差异时才会更改 DOM 节点。 例如,有一个组件,它每秒使用从父组件传递下来的不同属性重新渲染一次。注意,你可以添加一些文本到 <input> 标签,更新它的 value,但是文本不会在组件重渲染时消失:

React 的渲染和更新流程是其核心机制之一,它负责确保 UI 与应用状态保持一致。以下是详细的解析,包括渲染流程、更新流程,以及它们涉及的核心概念和机制。


一、React 渲染流程

当 React 应用第一次加载时,会经历以下的渲染流程:

1. 初次渲染

初次渲染是从根组件开始,递归构建整个组件树,生成 UI,直至页面呈现。

流程步骤:
  1. 创建 React 元素树: React 的 JSX 被转换成 JavaScript 对象(即 React 元素),这些元素描述了 UI 的结构。

  1. 构建 Fiber 树

    • React 会将 React 元素转换为内部的 Fiber 数据结构
    • Fiber 是 React 的核心工作单元,表示组件的信息和其状态。
  2. Reconciliation (协调过程)

    • React 开始从根组件递归遍历,调用组件的 render 方法,生成虚拟 DOM。
    • 如果是函数组件,会直接执行该函数;如果是类组件,会实例化类,调用其 render 方法。
  3. Commit 阶段 (提交真实 DOM)

    • 将生成的虚拟 DOM 转换为真实 DOM,并挂载到页面。
    • 在这一阶段,React 会执行副作用(如 useEffectcomponentDidMount)。

    主要分为两部分

    • Before Mutation:更新前的一些操作(例如记录 DOM 状态)。
    • Mutation:操作真实 DOM(如插入、更新或删除 DOM 节点)。

 

二、React 更新流程

当组件的 stateprops 发生变化时,会触发更新流程。React 的更新流程主要包括以下步骤:

1. 触发更新

  • 当组件调用 setState 或接收新的 props 时,React 标记该组件为需要更新。

2. Scheduler 调度更新

  • 调度优先级:React 根据任务的优先级(例如用户输入的更新比动画更紧急)决定何时开始工作。
  • React 依赖 时间切片 (Time Slicing),将任务分成多个小块,并在浏览器空闲时执行。

3. Reconciliation (协调过程)

  • 比较更新前后的 Fiber 树(即 Diff 算法)。
  • 如果两个 Fiber 节点表示相同的元素,保留原有的 DOM 节点;如果不同,则生成新的 DOM 节点。

主要策略

  • 类型相同的节点:更新属性并复用 DOM 节点。
  • 类型不同的节点:销毁旧节点,创建新节点。
  • 列表的 Diff 算法:根据 key 属性优化节点的复用。

4. Commit 阶段

  • 完成虚拟 DOM 的更新后,React 提交更改到真实 DOM。
  • 执行 DOM 操作(如插入、删除、更新)。
  • 执行副作用(如 useEffectcomponentDidUpdate

 

三、React 的核心内容详解

1. Fiber 架构

  • Fiber 是 React 16 引入的调度算法,核心目标是实现 可中断的渲染
  • 每个组件对应一个 Fiber 节点,记录了组件的类型、状态、DOM 引用等。

特点

  • 可中断:React 将渲染分成多个任务,可以随时暂停或恢复。
  • 优先级:不同任务有不同的优先级(如用户输入 > 数据加载)。

2. 时间切片 (Time Slicing)

  • React 使用 requestIdleCallbackMessageChannel 模拟时间切片。
  • 当渲染任务较重时,React 会暂停渲染,将控制权交还给浏览器,保证界面流畅性。

3. Reconciliation (Diff 算法)

  • React 使用了一种 O(n) 的算法,而不是传统虚拟 DOM 的 O(n^3)
  • 核心是通过 key 和节点类型快速判断节点是否需要更新。

4. Scheduler 调度

  • React 内置 Scheduler,通过任务优先级和调度机制实现任务分配。
  • 优先级分类
    • Immediate:同步任务(如 setState 的同步模式)。
    • User-blocking:如用户输入。
    • Normal:常规更新。
    • Low:非关键更新。
    • Idle:空闲时执行的任务。

5. Hooks 的依赖收集

  • 对于 useEffectuseMemo,React 会根据依赖数组 [dependencies] 判断是否需要重新执行。

 

四、示例讲解:从渲染到更新

初次渲染示例

function App() {
  const [count, setCount] = React.useState(0);
  return (
    <div>
      <h1>Count: {count}</h1>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
}
  1. 初次渲染

    • 调用 App 函数,生成虚拟 DOM。
    • 构建 Fiber 树,将虚拟 DOM 提交为真实 DOM。
  2. 点击更新

    • 点击按钮触发 setCount,React 标记 App 为需要更新。
    • React 比较前后 Fiber 树,发现 h1 的内容需要更新。
    • 提交更改,仅更新 h1 的内容,而不会重新创建整个 DOM。

五、优化点

  1. 避免不必要的更新
    • 使用 React.memouseMemo 缓存组件和计算结果。
  2. 合理设置 key
    • 为列表的每个元素分配唯一且稳定的 key,避免重复渲染。
  3. 按需加载
    • 使用动态加载(如 React.lazy)减小初次渲染的体积。

———————————————————————————————————————————

六、值得关注的点 

在React的渲染流程中,除了基本的流程框架外,还有一些细节值得关注,它们对于理解和优化React应用具有重要意义。以下是一些关键的细节:

1. 渲染流程的阶段划分

  • Render阶段(协调阶段):这个阶段主要是进行虚拟DOM的比较和更新计划的制定。React会在这个阶段调用组件的render方法(或函数体),生成新的虚拟DOM,并与旧的虚拟DOM进行比较,找出差异点,并标记需要更新的Fiber节点。
  • Commit阶段:这个阶段主要是将更新同步到实际的DOM中。React会在这个阶段执行DOM操作,例如创建、更新或删除DOM元素,以反映组件树的最新状态。

2. Fiber架构的引入

  • React 16引入了Fiber架构,实现了异步可中断的渲染。Fiber将渲染工作分解为一个个小的任务单元,每个任务单元称为Fiber节点。
  • 在渲染过程中,如果有更高优先级的任务出现(如用户交互),渲染可以被中断,优先处理高优先级任务。这使得React能够更好地响应用户操作,提供更流畅的用户体验。

3. 调度器(Scheduler)的作用

  • 调度器负责为任务排序优先级,让优先级高的任务先进入到协调器进行处理。
  • 调度器在浏览器的原生API中有类似的实现(如requestIdleCallback),但React团队为了实现更稳定和兼容的调度机制,自己实现了一套调度器。

4. 副作用(Effects)的处理

  • 在React中,副作用是指那些会影响组件外部状态的操作,如数据获取、订阅事件、手动修改DOM等。
  • 在Render阶段,React会收集这些副作用,并在Commit阶段执行它们。这样可以确保副作用在DOM更新之后执行,避免潜在的竞争条件。

5. 虚拟DOM的比较算法(Diff算法)

  • React使用了一种高效的Diff算法来比较新旧虚拟DOM树的差异。
  • 该算法采用了深度优先遍历和启发式算法等优化策略,以减少不必要的DOM操作和提高渲染性能。

6. 批量更新和异步执行

  • React的setState机制是“批量更新”和“异步执行”的。
  • 这意味着不会因为每个setState调用立即触发重新渲染,而是在所有更新完成后,进行一次统一的重新渲染。这有助于减少不必要的频繁渲染,提高应用的性能和响应性。

7. 错误边界(Error Boundaries)

  • React提供了错误边界组件来捕获组件渲染过程中的错误。
  • 错误边界组件是一个具有特定生命周期方法的类组件(如getDerivedStateFromError和componentDidCatch)。当子组件在渲染过程中抛出错误时,错误边界组件可以捕获该错误,并展示一个备用的UI,而不是让整个应用崩溃。

8. 渲染流程的中断和恢复

  • Render阶段的工作流程是可以随时被打断的,原因可能包括有其他更高优先级的任务需要执行、当前的time slice没有剩余的时间等。
  • 由于Render阶段的工作是在内存里面进行的,不会更新宿主环境UI,因此这个阶段即使工作流程反复被中断,用户也不会看到更新不完全的UI。

综上所述,React的渲染流程是一个复杂而高效的过程,涉及多个阶段和细节。通过深入理解这些细节,可以更好地优化React应用的性能和响应性,提高用户体验。