React从入门到出门第三章 虚拟 DOM 与并发渲染基础

大家好~ 前面我们已经掌握了 React 19 的函数组件、JSX 和核心 Hooks,今天咱们深入 React 的底层核心机制------虚拟 DOM(Virtual DOM)与并发渲染(Concurrent Rendering)。

很多刚接触 React 的同学会觉得这两个概念很抽象,甚至觉得"没必要了解,会用 API 就行"。但其实搞懂它们,能帮我们:

  • 理解"为什么 React 渲染性能好";
  • 写出更符合 React 设计理念的代码(避免不必要的性能问题);
  • 看懂后续高阶知识点(如 Fiber 架构、性能优化方案)。

今天这篇文章,我们就从"是什么→为什么→怎么工作"三个维度,用通俗的语言+直观的图例+简单的代码示例,把虚拟 DOM 和并发渲染的基础逻辑讲透,让新手也能轻松理解~

一、先搞懂:虚拟 DOM 到底是什么?

1. 从真实 DOM 的痛点说起

我们知道,浏览器中的真实 DOM(文档对象模型)是页面的"骨架",它不仅包含了元素的结构,还自带大量属性和方法。但真实 DOM 有个明显的痛点:操作真实 DOM 代价很高

比如我们要更新一个列表中的某一项数据,直接操作真实 DOM 时,浏览器会触发"重排"(重新计算元素位置)和"重绘"(重新绘制元素样式),这个过程会消耗大量性能------尤其是当页面元素很多、更新频繁时,很容易出现页面卡顿。

为了解决这个问题,React 引入了"虚拟 DOM"的概念。

2. 虚拟 DOM 的本质:DOM 的"轻量级副本"

虚拟 DOM(Virtual DOM)本质上是一个JavaScript 对象,它是对真实 DOM 的"抽象描述"------包含了真实 DOM 的元素类型、属性、子元素等关键信息,但去掉了真实 DOM 中冗余的属性和方法,是一个"轻量级"的副本。

举个例子:一个简单的真实 DOM 元素和对应的虚拟 DOM 对象对比:

3. React 中虚拟 DOM 的表现形式:JSX 的"幕后身份"

其实我们每天写的 JSX,最终都会被 Babel 转译为虚拟 DOM 对象。比如我们写的 JSX 代码:

javascript 复制代码
function App() {
  return (
    <div className="App">
      <h1>Hello Virtual DOM</h1>
      <p>React 19 虚拟 DOM 基础</p>
    </div>
  );
}

Babel 会把它转译为 React.createElement 函数的调用,而这个函数的返回值,就是虚拟 DOM 对象(也叫"React 元素"):

csharp 复制代码
// Babel 转译后的代码(简化版)
function App() {
  return React.createElement(
    'div', // type:元素类型
    { className: 'App' }, // props:元素属性
    // children:子元素(也是虚拟 DOM 对象)
    React.createElement('h1', null, 'Hello Virtual DOM'),
    React.createElement('p', null, 'React 19 虚拟 DOM 基础')
  );
}

// React.createElement 最终返回的虚拟 DOM 对象(简化)
{
  type: 'div',
  props: { className: 'App' },
  children: [
    { type: 'h1', props: {}, children: 'Hello Virtual DOM' },
    { type: 'p', props: {}, children: 'React 19 虚拟 DOM 基础' }
  ],
  // 其他内部属性(如 key、ref 等)
}

关键结论:JSX 是虚拟 DOM 的"语法糖",我们写 JSX 的过程,本质上是在描述虚拟 DOM 的结构。

二、虚拟 DOM 的核心工作流程:Diff 算法与批量更新

虚拟 DOM 之所以能提升性能,核心在于它的"先对比、后更新"策略------不是每次状态变化都直接操作真实 DOM,而是先通过"Diff 算法"对比新旧虚拟 DOM 的差异,再只把"有差异的部分"更新到真实 DOM 上。

整个流程可以分为 3 步:

1. 步骤 1:状态变化生成新的虚拟 DOM

当组件的状态(useState/useContext 等)发生变化时,React 会重新执行组件函数,生成一个新的虚拟 DOM 树(描述更新后的 UI 结构)。

2. 步骤 2:Diff 算法对比新旧虚拟 DOM 差异

React 会通过"Diff 算法"(差异检测算法)对比新旧两棵虚拟 DOM 树,找出"哪些节点发生了变化"(比如元素类型变化、属性变化、子元素变化等),并记录这些差异(形成"补丁")。

这里要注意:React 的 Diff 算法做了两个关键优化,让对比效率很高:

  • 同层对比:只对比同一层级的节点,不跨层级对比(比如 div 的子节点只和 div 的子节点对比,不会和 div 的父节点/子子节点对比),降低算法复杂度;
  • key 优化:对于列表节点,通过 key 标识节点唯一性,让 React 能快速定位到新增、删除或移动的节点(这也是为什么列表渲染要加 key 的核心原因)。

3. 步骤 3:批量更新真实 DOM

React 收集完所有差异后,会批量将这些差异对应的操作应用到真实 DOM 上------也就是"批量更新"。这样可以避免多次零散地操作真实 DOM,减少重排重绘的次数,从而提升性能。

用一个流程图直观展示整个流程:

实战感知:虚拟 DOM 的 Diff 优化

我们用一个简单的列表案例,感受一下 key 对 Diff 算法的优化作用(这也是日常开发中最容易接触到的虚拟 DOM 优化场景)。

反例:列表渲染不写 key(性能差)

javascript 复制代码
import { useState } from 'react';

function BadList() {
  const [list, setList] = useState([1, 2, 3]);

  const addItem = () => {
    setList([0, ...list]); // 在列表头部添加元素
  };

  return (
    <div>
      <button onClick={addItem}>在头部添加元素</button>
      <ul>
        {/* 不写 key,React 无法精准定位节点,会重新创建所有节点 */}
        {list.map(item => (
          <li>{item}</li>
        ))}
      </ul>
    </div>
  );
}

问题:不写 key 时,在列表头部添加元素后,React 无法区分新旧节点,会把原来的 1、2、3 节点全部销毁,再重新创建 0、1、2、3 节点,性能开销大。

正例:列表渲染写 key(性能优)

javascript 复制代码
import { useState } from 'react';

function GoodList() {
  const [list, setList] = useState([1, 2, 3]);

  const addItem = () => {
    setList([0, ...list]); // 在列表头部添加元素
  };

  return (
    <div>
      <button onClick={addItem}>在头部添加元素</button>
      <ul>
        {/* 写 key,React 能精准定位节点,仅新增 0 节点 */}
        {list.map(item => (
          <li key={item}>{item}</li> // key 为节点唯一标识
        ))}
      </ul>
    </div>
  );
}

优化效果:写 key 后,React 通过 key 能识别出"原来的 1、2、3 节点还在,只是位置变化了",只需新增 0 节点,并调整原有节点的位置,无需销毁重建,性能大幅提升。

三、并发渲染:React 19 流畅体验的核心保障

理解了虚拟 DOM 之后,我们再来看 React 19 的另一个核心机制------并发渲染(Concurrent Rendering)。如果说虚拟 DOM 解决了"如何高效更新 DOM"的问题,那并发渲染就解决了"如何让更新过程不阻塞用户交互"的问题。

1. 先理解:什么是"并发"?

"并发"简单来说就是:React 可以同时处理多个更新任务,但不会阻塞用户交互。比如用户在输入框打字的同时,页面正在进行大数据量列表的渲染------并发渲染能让输入操作保持流畅,不会因为列表渲染而卡顿。

在 React 18 之前,React 采用的是"同步渲染"模式:一旦开始渲染,就会一直执行到结束,期间无法中断,会阻塞所有用户交互(比如点击、输入等),这也是为什么复杂页面容易出现卡顿的原因。

而 React 19 继承并优化了 React 18 的并发渲染能力,让页面交互更流畅。

2. 并发渲染的核心:可中断、可恢复的渲染过程

并发渲染的核心原理是:将渲染过程拆分成多个小任务,React 可以根据任务的优先级,决定先执行哪个任务、暂停哪个任务,甚至放弃某个任务。当有更高优先级的任务(比如用户输入)进来时,React 可以暂停当前的低优先级任务(比如列表渲染),先处理高优先级任务,等用户交互完成后,再恢复低优先级任务的执行。

这里的"任务优先级"是 React 内部定义的,比如:

  • 高优先级:用户输入、点击按钮等实时交互;
  • 中优先级:网络请求完成后的数据更新;
  • 低优先级:非紧急的 UI 更新(比如列表滚动时的次要内容渲染)。

3. 并发渲染的关键:Fiber 架构(简化理解)

并发渲染之所以能实现"可中断、可恢复",核心依赖于 React 的 Fiber 架构------Fiber 是 React 16 之后引入的新架构,也是虚拟 DOM 的"升级版本"。

简单来说,Fiber 架构将虚拟 DOM 节点重新设计为"Fiber 节点",每个 Fiber 节点对应一个真实 DOM 节点,并且通过链表的形式串联起来。这种结构让 React 可以:

  • 将渲染任务拆分成每个 Fiber 节点的处理任务;
  • 在处理某个 Fiber 节点的过程中中断,记录当前进度;
  • 之后恢复进度,继续处理剩余的 Fiber 节点;
  • 如果任务不再需要(比如组件已经卸载),可以直接放弃剩余任务,避免无用功。

用一个流程图理解并发渲染的任务调度过程:

实战感知:并发渲染的流畅体验

我们用一个"大数据量渲染 + 输入框输入"的案例,感受并发渲染的优势。如果没有并发渲染,输入框会因为大数据量渲染而卡顿;有了并发渲染,输入会保持流畅。

javascript 复制代码
import { useState, useTransition } from 'react';

// 模拟大数据量(10000 条数据)
const mockData = Array.from({ length: 10000 }, (_, i) => ({
  id: i,
  content: `数据项 ${i + 1} - ${Math.random().toString(36).substring(2, 6)}`
}));

function ConcurrentRenderDemo() {
  const [inputValue, setInputValue] = useState('');
  const [filterText, setFilterText] = useState('');
  // useTransition:标记低优先级任务
  const [isPending, startTransition] = useTransition();

  // 输入框输入(高优先级任务)
  const handleInputChange = (e) => {
    setInputValue(e.target.value);
    // 将筛选数据的任务标记为低优先级
    startTransition(() => {
      setFilterText(e.target.value);
    });
  };

  // 根据筛选条件过滤数据(低优先级任务)
  const filteredData = mockData.filter(item => {
    return item.content.includes(filterText);
  });

  return (
    <div>
      <input
        type="text"
        value={inputValue}
        onChange={handleInputChange}
        placeholder="输入关键词筛选(感受流畅输入)..."
        style={{ width: '300px', height: '30px', padding: '0 8px' }}
      />
      <p>{isPending ? '筛选中...' : ''}</p>
      <div style={{ marginTop: '20px' }}>
        {filteredData.slice(0, 10).map(item => (
          <p key={item.id}>{item.content}</p>
        ))}
      </div>
    </div>
  );
}

效果说明:在这个案例中,输入框输入是高优先级任务,大数据量筛选是低优先级任务。通过 useTransition 标记后,React 会优先处理输入操作,让输入框保持流畅;筛选任务在后台异步执行,期间页面不会卡顿,筛选完成后再更新筛选结果。这就是并发渲染带来的流畅体验。

四、核心总结:虚拟 DOM 与并发渲染的关系

最后我们用几句话总结今天的核心知识点,帮大家理清两者的关系:

  1. 虚拟 DOM 是"数据结构" :是对真实 DOM 的抽象描述,核心作用是通过 Diff 算法减少真实 DOM 操作,提升更新效率;
  2. 并发渲染是"调度机制" :是对渲染任务的优先级管理,核心作用是通过可中断、可恢复的渲染过程,避免阻塞用户交互,提升体验;
  3. 两者相辅相成:虚拟 DOM 为并发渲染提供了"可拆分的更新单元"(Fiber 节点),并发渲染让虚拟 DOM 的更新过程更"智能",两者共同构成了 React 19 高性能、高流畅度的基础。

五、常见误区与避坑指南

  • 误区 1:虚拟 DOM 一定比真实 DOM 快:不一定!对于简单的、少量的 DOM 更新,直接操作真实 DOM 可能更快(虚拟 DOM 有 Diff 和批量更新的额外开销)。虚拟 DOM 的优势在于"大量、频繁更新"的场景;
  • 误区 2:并发渲染会让所有更新变快:不会!并发渲染的核心是"提升体验流畅度",不是"提升更新速度"。它通过优先级调度,让高优先级任务优先执行,避免低优先级任务阻塞用户交互;
  • 误区 3:key 可以随便用(比如用 index) :不可以!index 作为 key 时,若列表有新增、删除、排序操作,key 会跟着变化,导致 React 误判节点变化,反而降低性能。key 必须是节点的"唯一且稳定"的标识(如后端返回的 id);
  • 误区 4:不用关心底层机制,会用 API 就行:对于简单项目可能没问题,但对于复杂项目的性能优化、问题排查,理解虚拟 DOM 和并发渲染的基础逻辑至关重要(比如知道为什么 useTransition 能优化体验)。

六、下一步学习方向

今天我们掌握了虚拟 DOM 与并发渲染的基础逻辑,下一步可以重点学习:

  • Fiber 架构的详细原理:深入理解 Fiber 节点的结构、链表遍历方式;
  • React 19 的性能优化方案:比如 memo、useMemo、useCallback 与虚拟 DOM/Diff 算法的配合;
  • 并发渲染的高级 API:如 useDeferredValue、Suspense 与并发渲染的结合使用。

如果这篇文章对你有帮助,欢迎点赞、收藏、转发~ 有任何问题也可以在评论区留言交流~ 我们下期再见!

相关推荐
代码猎人3 分钟前
substring和substr有什么区别
前端
pimkle3 分钟前
visactor vTable 在移动端支持 ellipsis 气泡
前端
donecoding3 分钟前
告别 scrollIntoView 的“越级滚动”:一行代码解决横向滚动问题
前端·javascript
0__O3 分钟前
如何在 monaco 中实现自定义语言的高亮
前端·javascript·编程语言
Jasmine_llq5 分钟前
《P3200 [HNOI2009] 有趣的数列》
java·前端·算法·线性筛法(欧拉筛)·快速幂算法(二进制幂)·勒让德定理(质因子次数统计)·组合数的质因子分解取模法
呆头鸭L6 分钟前
快速上手Electron
前端·javascript·electron
Aliex_git10 分钟前
性能指标笔记
前端·笔记·性能优化
秋天的一阵风11 分钟前
🌟 藏在 Vue3 源码里的 “二进制艺术”:位运算如何让代码又快又省内存?
前端·vue.js·面试
松涛和鸣11 分钟前
48、MQTT 3.1.1
linux·前端·网络·数据库·tcp/ip·html
helloworld也报错?12 分钟前
保存网页为PDF
前端·javascript·pdf