Skip to content

React 虚拟 DOM 与 Diff 算法详解

虚拟 DOM 的概念

虚拟 DOM(Virtual DOM)是 React 中的一个核心概念,它是对真实 DOM 的一种轻量级抽象表示。虚拟 DOM 本质上是一个 JavaScript 对象,用于描述真实 DOM 的结构和属性。

虚拟 DOM 的优势

  1. 性能优化:将多次DOM操作合并为一次批量更新,减少重排和重绘减少直接操作真实 DOM 的次数,提高渲染性能,“修改 10000 条列表中的一条” 的问题。 如果手动优化原生 JS,确实可以做到比虚拟 DOM 快。 但是,在一个大型、动态、频繁交互的应用中, 手动保证每一次 DOM 操作都是最优的,是极其困难甚至不可能的。 虚拟 DOM 提供了一个确定性的性能保障:它通过批量更新、异步渲染、高效的 diff 算法, 将多次、分散的 DOM 操作合并,避免了 “布局抖动” 和冗余重绘。 它把性能的下限兜住了,让开发者不用在写业务代码时,时刻担心 “我是不是又多了一次回流”

  2. 跨平台:可以在不同平台上使用相同的组件代码,虚拟 DOM 最大的价值是 “跨平台” 与 “声明式” 虚拟 DOM 是对真实 UI 的抽象。它不仅仅是一个 JS 对象, 更是一个描述 UI 状态的中间层。有了这个中间层,React 才能实现: 跨平台渲染:同样的虚拟 DOM 结构,可以渲染到 Web(真实 DOM)、 iOS/Android(React Native)、甚至命令行终端。

  3. 声明式 UI:开发者只需要描述 UI “应该长什么样”, 框架负责把声明式代码转换成命令式的 DOM 操作。 这带来的可维护性、可预测性的提升,远比那点性能增量重要得多

  4. diff 算法的核心逻辑与优化: 同层对比:React 的 diff 算法不会跨层级比较, 这大大降低了算法复杂度,从 O (n³) 优化到了 O (n)。 Key 的作用:Key 不是给后端用的,而是给 diff 算法用的。 它能帮助 React 识别哪些元素是稳定的、被移动了、被新增或被删除, 从而复用真实 DOM 节点,减少不必要的销毁和重建。 真实性能瓶颈:虚拟 DOM 的性能瓶颈往往不在 diff 本身, 而在无用的 render 函数执行。优化手段是使用 React.memo、 useMemo、useCallback 来避免子组件的无效渲染。

虚拟 DOM 的结构

在 React 中,虚拟 DOM 通常由 React.createElement 函数创建,或者通过 JSX 语法转换而来。一个典型的虚拟 DOM 节点结构如下:

javascript
const vnode = {
  type: 'div', // 元素类型
  props: {
    // 属性
    className: 'container',
    children: [
      // 子节点
      {
        type: 'h1',
        props: {
          children: 'Hello React'
        }
      }
    ]
  }
}

虚拟 DOM 的性能优势

1. 减少 DOM 操作

虚拟 DOM 可以将多次 DOM 操作合并为一次,减少了浏览器的重排和重绘。

2. 批量更新

React 会将多个状态更新合并为一次渲染,提高了渲染性能。

3. 跨平台支持

虚拟 DOM 可以在不同平台上使用相同的组件代码,如 React Native。

4. 简化开发

虚拟 DOM 提供了声明式的 API,使开发更加直观和简洁。

Diff 算法的概念

Diff 算法是 React 用于比较新旧虚拟 DOM 树差异的算法。它的目标是找出两个虚拟 DOM 树之间的差异,并只更新必要的部分,而不是重新渲染整个树。

Diff 算法的设计原则

  1. 同级比较:只比较同一层级的节点,不跨层级比较
  2. 类型比较:如果节点类型不同,直接替换整个节点
  3. key 比较:使用 key 属性来识别相同的节点,提高比较效率

Diff 算法的详细执行过程

1. 初始渲染

当 React 应用首次渲染时,会创建一个完整的虚拟 DOM 树,然后将其转换为真实 DOM 并插入到页面中。

2. 更新阶段

当组件状态或 props 发生变化时,React 会:

  1. 创建新的虚拟 DOM 树:根据新的状态或 props 生成新的虚拟 DOM 树
  2. 执行 Diff 算法:比较新旧虚拟 DOM 树的差异
  3. 生成更新补丁:根据差异生成最小的更新操作
  4. 应用更新:将更新补丁应用到真实 DOM 上

3. Diff 算法的具体步骤

步骤 1:同级比较

React 会从根节点开始,逐层比较新旧虚拟 DOM 树的节点:

javascript
function diff(oldVNode, newVNode) {
  // 如果节点类型不同,直接替换
  if (oldVNode.type !== newVNode.type) {
    return replaceNode(oldVNode, newVNode)
  }

  // 如果是文本节点,比较内容
  if (oldVNode.type === 'text') {
    if (oldVNode.props.children !== newVNode.props.children) {
      return updateTextNode(oldVNode, newVNode)
    }
    return null
  }

  // 比较属性
  const propsDiff = diffProps(oldVNode.props, newVNode.props)

  // 比较子节点
  const childrenDiff = diffChildren(
    oldVNode.props.children,
    newVNode.props.children
  )

  // 如果没有差异,返回 null
  if (!propsDiff && !childrenDiff) {
    return null
  }

  // 返回更新操作
  return {
    type: 'UPDATE',
    node: oldVNode,
    propsDiff,
    childrenDiff
  }
}

步骤 2:子节点比较

对于子节点的比较,React 采用了一种高效的算法,主要包括以下几种情况:

  1. 无 key 的情况:使用索引进行比较
  2. 有 key 的情况:使用 key 进行匹配,提高比较效率
javascript
function diffChildren(oldChildren, newChildren) {
  const diffs = []
  const oldChildrenMap = new Map()

  // 首先,将旧子节点按 key 或索引存储
  oldChildren.forEach((child, index) => {
    const key = child.props.key || index
    oldChildrenMap.set(key, { child, index })
  })

  // 然后,遍历新子节点
  newChildren.forEach((newChild, newIndex) => {
    const key = newChild.props.key || newIndex
    const oldChildInfo = oldChildrenMap.get(key)

    if (oldChildInfo) {
      // 如果找到相同 key 的节点,比较它们的差异
      const diff = diff(oldChildInfo.child, newChild)
      if (diff) {
        diffs.push({
          type: 'UPDATE',
          index: newIndex,
          diff
        })
      }
      // 从映射中删除,标记为已处理
      oldChildrenMap.delete(key)
    } else {
      // 如果没有找到相同 key 的节点,添加新节点
      diffs.push({
        type: 'ADD',
        index: newIndex,
        node: newChild
      })
    }
  })

  // 处理剩余的旧节点(需要删除的节点)
  oldChildrenMap.forEach(oldChildInfo => {
    diffs.push({
      type: 'REMOVE',
      index: oldChildInfo.index
    })
  })

  return diffs.length > 0 ? diffs : null
}

步骤 3:属性比较

比较节点的属性差异:

javascript
function diffProps(oldProps, newProps) {
  const propsDiff = {}
  let hasDiff = false

  // 检查新属性
  for (const key in newProps) {
    if (newProps.hasOwnProperty(key)) {
      if (oldProps[key] !== newProps[key]) {
        propsDiff[key] = newProps[key]
        hasDiff = true
      }
    }
  }

  // 检查删除的属性
  for (const key in oldProps) {
    if (oldProps.hasOwnProperty(key) && !newProps.hasOwnProperty(key)) {
      propsDiff[key] = undefined
      hasDiff = true
    }
  }

  return hasDiff ? propsDiff : null
}

React Diff 算法的优化策略

1. 同级比较原则

React 只比较同一层级的节点,不跨层级比较,这大大减少了比较的复杂度。

2. 类型比较原则

如果节点类型不同,React 会直接替换整个节点,而不是尝试比较子节点。

3. Key 属性的使用

使用 key 属性可以帮助 React 识别哪些节点是相同的,哪些是新增的,哪些是删除的,从而提高比较效率。

4. 列表渲染的优化

在渲染列表时,为每个列表项提供唯一的 key 属性,避免使用索引作为 key。

虚拟 DOM 与真实 DOM 的转换

1. 初次渲染

当 React 首次渲染时,会:

  1. 创建虚拟 DOM 树
  2. 调用 render 方法将虚拟 DOM 转换为真实 DOM
  3. 将真实 DOM 插入到页面中

2. 更新渲染

当组件状态或 props 变化时,React 会:

  1. 创建新的虚拟 DOM 树
  2. 执行 Diff 算法,找出差异
  3. 只更新有差异的部分,生成最小的 DOM 操作
  4. 执行这些 DOM 操作,更新真实 DOM

实际应用中的最佳实践

1. 使用合适的 key

在渲染列表时,使用唯一且稳定的 key,避免使用索引作为 key。

2. 避免不必要的渲染

使用 shouldComponentUpdateReact.memo 来避免不必要的渲染。

3. 合理使用状态

将状态放在合适的组件层级,避免状态提升过高导致的不必要渲染。

4. 使用函数组件和 Hooks

函数组件和 Hooks 提供了更简洁的语法和更好的性能优化能力。

虚拟 DOM 与 Diff 算法的局限性

1. 初始渲染性能

虚拟 DOM 在初始渲染时可能比直接操作 DOM 慢,因为需要额外的创建和比较过程。

2. 内存消耗

虚拟 DOM 会占用一定的内存,对于非常大的应用可能会有内存压力。

3. 复杂场景的优化

对于某些复杂场景,可能需要手动优化,而不是依赖默认的 Diff 算法。

总结

React 的虚拟 DOM 和 Diff 算法是其性能优化的核心。通过虚拟 DOM,React 可以减少直接操作真实 DOM 的次数,提高渲染性能。通过 Diff 算法,React 可以找出最小的更新操作,进一步提高性能。

虚拟 DOM 和 Diff 算法的设计思想不仅适用于 React,也对其他前端框架产生了深远的影响。理解这些概念,对于掌握 React 的工作原理和优化 React 应用的性能都非常重要。

基于 VitePress 的本地知识库