React渲染流程与更新diff算法

React 的渲染流程从虚拟 DOM 树的生成到真实 DOM 的挂载和更新是一个层层递进的过程。以下是详细的解析:


渲染流程概述

React 的渲染流程可以分为两个阶段:

  1. 初次渲染(Mounting): 将虚拟 DOM 树转换为真实 DOM,并挂载到页面。
  2. 更新渲染(Updating): 比较新旧虚拟 DOM 树(Diff),仅更新需要变更的部分。

这两个阶段的流程如下:


一、初次渲染(Mounting)

  1. 创建组件树并生成虚拟 DOM

    • 当 React 应用启动时,调用 React.createRoot(container).render(<App />)
    • React 递归调用组件树的 render 方法或 function,生成一个完整的虚拟 DOM 树。
    • 每个组件都会返回一个表示 UI 的虚拟 DOM 节点。

    示例:

    jsx 复制代码
    function App() {
      return (
        <div>
          <h1>Hello, React!</h1>
          <p>Welcome to the world of React.</p>
        </div>
      );
    }

    虚拟 DOM 树:

    javascript 复制代码
    {
      type: 'div',
      props: {
        children: [
          { type: 'h1', props: { children: 'Hello, React!' } },
          { type: 'p', props: { children: 'Welcome to the world of React.' } }
        ]
      }
    }
  2. 调和(Reconciliation)

    • React 将虚拟 DOM 树与当前页面的真实 DOM 进行比较(此时页面为空)。
    • 因为页面中没有 DOM,React 将虚拟 DOM 直接转化为真实 DOM。
  3. 生成真实 DOM 并挂载

    • React 遍历虚拟 DOM 树,使用 document.createElement 创建真实 DOM 节点。
    • 为每个节点设置属性(如 classNameid 等),并递归插入子节点。
    • 最终将构建好的 DOM 树挂载到指定的容器中。

    示例挂载代码:

    javascript 复制代码
    const container = document.getElementById('root');
    ReactDOM.createRoot(container).render(<App />);

二、更新渲染(Updating)

当组件的 stateprops 发生变化时,React 会重新渲染受影响的部分。此过程包括以下步骤:

1. 检测变化
  • 组件的状态(state)或属性(props)更新时,React 会触发组件的更新。
  • 调用组件的 render 方法或 function,生成新的虚拟 DOM 树。

示例:

jsx 复制代码
function Counter() {
  const [count, setCount] = React.useState(0);
  return (
    <div>
      <p>{count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
}
  • 初始虚拟 DOM:

    javascript 复制代码
    {
      type: 'div',
      props: {
        children: [
          { type: 'p', props: { children: 0 } },
          { type: 'button', props: { children: 'Increment', onClick: [Function] } }
        ]
      }
    }
  • 当点击按钮后,count 变为 1,生成新的虚拟 DOM:

    javascript 复制代码
    {
      type: 'div',
      props: {
        children: [
          { type: 'p', props: { children: 1 } },
          { type: 'button', props: { children: 'Increment', onClick: [Function] } }
        ]
      }
    }

2. 比较新旧虚拟 DOM(Diff)

React 使用 Diff 算法 比较新旧虚拟 DOM 树,找出变化部分。

关键步骤:

  1. 类型比较:

    • 如果节点类型(如 divspan)不同,React 直接移除旧节点并插入新节点。
    • 如果类型相同,比较属性和子节点。
  2. 属性比较:

    • 比较新旧节点的属性,仅更新有变化的属性。

    示例:

    javascript 复制代码
    Old: <div id="old" />
    New: <div id="new" />

    React 仅更新 id"new"

  3. 子节点比较:

    • React 使用 key 属性优化动态子节点的比较。
    • 如果没有 key,React 按序逐一比较,可能导致多余的重建。

3. 应用变化(Patch)
  • React 计算出需要的最小 DOM 操作(增、删、改、移)。
  • 批量更新 DOM,减少重绘和重排的次数。

示例:

  • 假设旧 DOM 是:

    html 复制代码
    <div>
      <p>0</p>
      <button>Increment</button>
    </div>
  • 新虚拟 DOM 变为:

    html 复制代码
    <div>
      <p>1</p>
      <button>Increment</button>
    </div>
  • React 会:

    1. 更新 <p> 元素的文本内容从 0 改为 1
    2. 不会重新创建 button 元素。

三、Fiber 架构

在 React 16+ 中,更新阶段由 Fiber 架构 驱动,以提高性能。

1. Fiber 的作用
  • 将渲染工作分解为多个小任务(可中断的工作单元)。
  • 实现优先级机制,高优先级任务(如用户交互)可打断低优先级任务(如后台渲染)。
2. Fiber 渲染流程
  1. 渲染阶段(Render Phase): 构建新的 Fiber 树,比较新旧 Fiber 树,计算出需要的更新。

    • 此阶段是纯粹的计算,不会直接操作 DOM。
    • 可中断,React 会优先处理高优先级任务。
  2. 提交阶段(Commit Phase): 将更新应用到真实 DOM。

    • 这是不可中断的过程,React 将计算出的差异(Patch)批量提交到 DOM。

总结

React 的渲染流程从虚拟 DOM 到真实 DOM,大致可以分为以下步骤:

  1. 初次渲染:

    • 生成虚拟 DOM 树。
    • 调和并挂载真实 DOM。
  2. 更新渲染:

    • 检测状态或属性变化。
    • 生成新虚拟 DOM 树。
    • Diff 算法比较新旧虚拟 DOM,计算最小变更。
    • 使用 Fiber 提高性能,将更新应用到真实 DOM。

这种分阶段的设计使得 React 能够高效地渲染和更新 UI,同时保持良好的用户体验。


React 的 Diff 算法 是其核心性能优化技术,用于比较新旧虚拟 DOM 树的差异,并以最小的代价更新真实 DOM。为了保证效率,React 并没有采用传统的暴力对比方法(时间复杂度为 (O(n^3))),而是结合特定的假设对 Diff 过程进行优化,将时间复杂度降低到 (O(n))。

以下是 Diff 算法的详解:


Diff 算法的优化假设

React 的 Diff 算法基于以下三个假设来简化比较过程:

  1. 不同类型的节点产生完全不同的树

    • 如果两个节点类型(如 divspan)不同,React 会直接销毁旧节点及其子节点,重新创建新节点,而不是逐一比较子节点。
  2. 开发者可通过唯一的 key 标识子节点

    • 当比较同一层级的子节点时,React 假定它们的顺序可能会改变,因此会利用 key 来快速定位变化节点。
  3. 同级子节点只与同级的其他子节点进行比较

    • React 不会跨层级比较节点。比如,新旧虚拟 DOM 树中不同层级的节点不会被直接比较。

Diff 算法的过程

Diff 算法分为以下几个步骤:


1. 树的层次比较
  • React 会逐层比较新旧虚拟 DOM 树,而不会跨层级进行。
  • 如果根节点类型不同,直接替换整个子树。

示例:

旧虚拟 DOM:

jsx 复制代码
<div>
  <p>Old</p>
</div>

新虚拟 DOM:

jsx 复制代码
<span>
  <p>New</p>
</span>

结果:div 被替换为 span,整个子树重新创建。


2. 同类型节点的属性比较
  • 对于同类型的节点(如两个 div),React 会逐个比较其属性,并更新有变化的部分。

示例:

旧节点:

jsx 复制代码
<div id="old" className="foo"></div>

新节点:

jsx 复制代码
<div id="new" className="foo"></div>

结果:只更新 id 属性。


3. 子节点的比较

子节点的比较是 Diff 算法的核心。React 会根据子节点是否有 key 来采取不同的策略。


3.1 无 key 的子节点
  • React 逐一比较新旧子节点的位置:
    1. 如果新节点和旧节点的类型相同,递归比较它们的属性和子节点。
    2. 如果类型不同,移除旧节点并插入新节点。

示例:

旧子节点:

jsx 复制代码
<ul>
  <li>Apple</li>
  <li>Banana</li>
</ul>

新子节点:

jsx 复制代码
<ul>
  <li>Banana</li>
  <li>Apple</li>
</ul>

结果:React 假定第一个节点由 Apple 变为 Banana,第二个由 Banana 变为 Apple,导致两个节点被完全重建。


3.2 有 key 的子节点
  • key 提供了稳定的标识,React 可以通过 key 快速找到对应节点,避免不必要的重建。

示例:

旧子节点:

jsx 复制代码
<ul>
  <li key="a">Apple</li>
  <li key="b">Banana</li>
</ul>

新子节点:

jsx 复制代码
<ul>
  <li key="b">Banana</li>
  <li key="a">Apple</li>
</ul>

结果:

  1. React 检测到 key="b" 的节点仍存在,只需更新位置。
  2. 同样,key="a" 的节点只更新位置。

这种方式避免了重建节点,仅调整顺序。


4. 插入、删除、移动

在比较过程中,React 会记录以下操作:

  • 插入新节点:当新节点在旧节点中不存在时,React 会创建并插入它。
  • 删除旧节点:当旧节点在新节点中不存在时,React 会删除它。
  • 移动节点 :当 key 相同但位置变化时,React 会调整节点位置。

操作流程:

  1. 生成差异的补丁(Patch)。
  2. 最小化 DOM 操作,应用这些补丁。

Diff 算法的实现机制

React 的 Diff 算法是基于以下过程实现的:

  1. 单节点 Diff

    • 比较节点类型,相同则继续比较属性和子节点。
    • 不同则替换节点。
  2. 多节点 Diff

    • 如果有 key,通过 key 映射对子节点进行高效比较。
    • 如果无 key,按顺序比较,可能导致不必要的重建。
  3. 最小化操作

    • React 会批量执行必要的 DOM 更新,减少浏览器的重绘和重排次数。

关键优化点

  1. Key 的使用:

    • 在动态列表中,使用唯一的 key,可显著提高 Diff 效率。
    • 不推荐使用索引作为 key,因为顺序改变时可能导致不必要的更新。
  2. 分层比较:

    • React 限制比较在同一层级进行,避免了跨层级的复杂计算。
  3. 批量更新:

    • React 使用批处理(Batching)技术,将多次状态更新合并为一次操作,降低性能开销。

总结

React 的 Diff 算法是为了高效地更新真实 DOM,遵循以下原则:

  • 逐层比较,避免跨层级复杂度。
  • 利用 key 优化动态子节点的对比。
  • 尽量减少实际的 DOM 操作。

通过这些优化策略,React 能够在性能与灵活性之间取得良好的平衡。

相关推荐
恋猫de小郭16 分钟前
Flutter Zero 是什么?它的出现有什么意义?为什么你需要了解下?
android·前端·flutter
崔庆才丨静觅7 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60618 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了8 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅8 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅8 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅8 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment9 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅9 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊9 小时前
jwt介绍
前端