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 能够在性能与灵活性之间取得良好的平衡。

相关推荐
松树戈12 分钟前
JS推荐实践
开发语言·javascript·ecmascript
vener_19 分钟前
LuckySheet协同编辑后端示例(Django+Channel,Websocket通信)
javascript·后端·python·websocket·django·luckysheet
草字25 分钟前
uniapp input限制输入负数,以及保留小数点两位.
java·前端·uni-app
老码沉思录31 分钟前
React Native 全栈开发实战班 - 性能与调试之打包与发布
javascript·react native·react.js
没有黑科技44 分钟前
基于web的音乐网站(Java+SpringBoot+Mysql)
java·前端·spring boot
前端初见1 小时前
彻底搞懂前端环境变量使用和原理
前端
小王码农记1 小时前
vue中路由缓存
前端·vue.js·缓存·typescript·anti-design-vue
大G哥1 小时前
我用豆包MarsCode IDE 做了一个 CSS 权重小组件
前端·css
乐闻x1 小时前
Vue实践篇:如何在 Vue 项目中检测元素是否展示
前端·javascript·vue.js
麻花20132 小时前
WPF里面的C1FlexGrid表格控件添加RadioButton单选
java·服务器·前端