前端面霸:图解助你掌握虚拟 DOM 面试点

1,155 阅读10分钟

一、虚拟 DOM

截屏2023-05-08 11.25.11.png

在React框架中,虚拟 DOM 是由 React.createElement() 或 JSX 语法创建的 JavaScript 对象。

当应用程序的状态发生更改时,它可以在内存中构建新的 DOM 树,与旧的 DOM 树进行比较,以确定需要进行哪些 DOM 操作来更新应用程序的界面。

工作原理

虚拟 DOM 的工作原理分为三个步骤:

  1. 生成虚拟 DOM 树
  2. 比较新旧虚拟 DOM 树以查找差异
  3. 根据差异更新实际 DOM 元素

React框架使用一种叫做 "diff算法" 的优化算法来查找差异,并使用 "批处理" 技术将多个 DOM 更新操作合并为单个操作来优化性能。

<div className='Index'>
  <div>Hello World</div>
  <ul>
    <li>React</li>
    <li>Vue</li>
  </ul>
</div>

// 经过转化后,长这样:
{
  type: 'div',
    props: { class: 'Index' },
  children: [
    {
      type: 'div',
      children: 'Hello World'
    },
    {
      type: 'ul',
      children: [
        {
          type: 'li',
          children: 'React'
        },
        {
          type: 'li',
          children: 'Vue'
        },
      ]
    }
  ]
}

在React框架中,每个虚拟DOM节点都有以下属性:

  • type: 节点的类型,例如元素节点、文本节点等。
  • ref: 节点的引用,用于在React中直接访问DOM节点。
  • key: 用于帮助React区分不同的子元素,以提高更新性能。
  • props: 节点的属性,例如元素节点的class、id等属性。
  • children: 包含节点的子节点的数组,其中子节点可以是其他虚拟DOM节点,也可以是文本节点。如果一个节点没有子节点,则其children属性为空数组。
  • $$typeof: React使用的一个特殊属性,用于标识该节点是一个合法的React元素,而不是普通的JavaScript对象。

二、与真实 DOM 对比

通过各自的创建方式,以打印的方式检查两种 DOM 的区别:

const VDOM = React.createElement('div', {}, 'Hello VDOM')
const DOM = document.createElement("div");
DOM.innerHTML = 'Hello DOM'

console.log(`虚拟DOM:`, VDOM)
console.log(`真实DOM:`, DOM)

数据结构不同

  • 虚拟 DOM 是 JavaScript 对象
  • 而真实 DOM 是浏览器内部维护的一种数据结构。

操作方式不同

  • 虚拟 DOM 的操作是在 JavaScript 环境下进行的,操作通常不会引起页面的重绘和重排
  • 而真实 DOM 的操作是在浏览器环境下进行的,操作很可能会引起页面的重绘和重排

更新方式不同

  • 虚拟 DOM 更新时,会先生成新的虚拟 DOM 树,然后通过比较新旧虚拟 DOM 树的差异来更新真实 DOM
  • 而真实 DOM 更新时,每次修改都会立即更新 DOM 树

三、虚拟 DOM 的优点

  1. 提高性能:浏览器处理 DOM 很慢,处理 JS 对象很快 使用虚拟 DOM,可以避免频繁地对实际 DOM 进行操作,从而减少浏览器的重绘和回流,提高应用程序的性能和效率。
  2. 简化开发:原生JS很多时候要关注 DOM 操作,对于虚拟 DOM 开发者只需要关注数据和状态的变化,而不必考虑如何手动更新 DOM 。React 会负责一切与 DOM 相关的操作,包括处理事件、调整布局、更新样式等。
  3. 跨平台:由于虚拟 DOM 只是一个 JavaScript 对象,因此它可以在不同的平台上运行,例如Web、iOS 和 Android 等。这使得开发人员可以在不同的平台上使用相同的代码库,并且只需根据需要使用不同的渲染器即可。

综上所述,虚拟 DOM 可以提高性能、简化开发、跨平台和方便测试等方面的优点,使得现代 Web 应用程序开发更加高效和可靠。

一定能提升性能吗

不一定!

虚拟 DOM 的性能优势主要在于能够减少实际 DOM 的操作次数。但是,如果应用程序本身的复杂度不高或者虚拟DOM的实现方式不够优秀,可能无法带来性能提升,甚至会引入额外的性能开销。

它的优势是在于 diff 算法和批量处理策略,将所有的 DOM 操作搜集起来,一次性去改变真实的 DOM ,但在首次渲染上,虚拟 DOM 会多了一层计算,消耗一些性能,所以有可能会比 html 渲染的要慢

总之,虚拟DOM在适当的情况下可以提高性能,但并不是一定能够提升性能,需要根据实际情况进行评估。

四、diff 算法

截屏2023-05-08 11.26.04.png

diff 算法是指在比较两棵虚拟DOM树时,通过一系列的算法来计算出它们之间的差异,然后只更新需要更新的部分,从而减少对实际DOM的操作,提高性能和效率。

降低 diff 算法复杂度

React 中的 diff 算法并非首创,而是引入,React团队为 diff 算法做出了质的优化:

在计算一颗树转化为另一颗树有哪些改变时,传统的 diff 算法通过循环递归对节点进行依此对比,算法复杂度达到 O(n^ 3) ,也就是说,如果展示一千个节点,就要计算十亿次。

而 Reac t中的 diff 算法,算法复杂度为 O(n) ,即展示一千个节点,只需要计算一千次。

React通过三种策略完成了优化

  1. Web UI 中 DOM 节点跨层级的移动操作特别少,可以忽略不计。
  2. 拥有相同类的两个组件将会生成相似的树形结构,拥有不同类的两个组件将会生成不同的树形结构。
  3. 对于同一层级的一组子节点,它们可以通过唯一 id 进行区分。

分别对应:tree diff(同级比较)、component diff(组件比较)、element diff(节点比较)

当应用程序的状态发生变化时,虚拟DOM会在内存中构建新的虚拟DOM树,并与之前的虚拟DOM树进行比较,以找出它们之间的差异,然后只更新需要更新的部分,从而提高应用程序的性能和效率。这个过程称为 “协调”

Tree diff

tree diff 是虚拟 DOM 协调过程中的一种算法,用于查找并比较新旧虚拟DOM树之间的差异。它通过深度优先遍历虚拟DOM树,并逐个比较节点来查找差异,并标记需要更新的部分,从而提高应用程序的性能和效率。

Component diff

Component diff算法的实现方式与tree diff算法类似,都是采用深度优先遍历的方式来遍历虚拟DOM树。但是,Component diff算法比tree diff算法更加复杂,因为它不仅要比较虚拟DOM节点之间的差异,还要比较组件的状态和属性。在比较组件时,React会根据组件类型和key值来确定它们是否相同,从而决定是否需要更新组件。

element diff

Element diff算法的实现方式比Component diff算法更加简单,它只需要比较同一层级的子节点之间的key值,以确定它们的位置是否有变化。如果子节点的位置没有变化,则只需要比较其它属性是否有变化,并更新需要更新的部分。如果子节点的位置有变化,则需要将原来的子节点移动到新的位置,而不是创建一个新的子节点。

五、虚拟DOM面试题

01 | 虚拟DOM是如何提高应用程序性能的?

提高性能的本质都是为了:减少操作真实 DOM 以减少性能消耗, 方法有两类:

  1. 通过将 DOM 操作转换为对 JavaScript 对象的操作
  • 因为真实 DOM 的操作会引起页面的重绘和重排,这是非常消耗性能的。而虚拟 DOM 操作只需要更新 JavaScript 对象,不会引起页面的重绘和重排。
  1. 通过 diff 算法等优化策略
  • 在更新虚拟 DOM 树时,会通过 diff 算法比较新旧虚拟DOM树的差异,只更新需要更新的部分,从而减少对真实DOM的操作次数。
  • 虚拟 DOM 还使用批处理技术,将多个DOM操作合并为单个操作,从而进一步提高性能。

批处理:需要进行多次DOM操作,批处理技术可以将这些操作合并为单个操作,从而减少对真实DOM的操作次数,提高性能和效率。


02 | 为什么处理DOM慢,而处理对象快?

浏览器处理 DOM 很慢的原因主要有以下几点:

  1. DOM 操作会引起页面的重绘和重排,这是非常消耗性能的。每次对 DOM 进行修改都需要重新计算布局和重新绘制元素,这个过程非常耗费时间。
  2. DOM 结构是树形结构,它需要通过遍历来查找和访问节点。当 DOM 结构非常庞大时,遍历的时间成本也会相应增加。
  3. DOM 操作涉及到网络请求和 I/O 操作,这些操作通常是异步执行的,需要等待操作完成后才能进行下一步操作。这也会影响到 DOM 操作的性能。

相比之下,JS处理对象要快一些的原因主要有以下几点:

  1. JS 引擎通常会将对象存储在堆内存中,并使用指针来访问它们。因为堆内存是连续的,所以访问对象的时间复杂度是常数级别的,而不会像 DOM 遍历那样需要花费大量时间。
  2. JS 引擎在对对象进行操作时,通常会将其存储在寄存器或缓存中,这样可以大大提高访问速度。
  3. JS 的对象通常比DOM结构更小,这意味着访问和操作对象所需的数据量更少,也更容易缓存。

总之,浏览器处理 DOM 很慢,而 JS 处理对象很快的原因主要在于 DOM 操作涉及到页面布局和渲染等复杂的计算和 I/O 操作,而 JS 操作对象通常只需要访问内存中的指针,并且对象的大小和操作数据量都比 DOM 结构小。


03 | 什么是key值?在React中,为什么需要使用key值?

在 React 中,key 是用来标识列表中每个子元素的唯一标识符。当使用列表渲染(如 'map()' 方法)时,React 会根据每个子元素的 key 值来进行优化,从而提高列表的渲染性能和效率。

React 使用 key 来追踪哪些子元素被修改、添加或删除。当进行列表更新时,React 会首先使用 key 来判断新旧子元素是否相同,从而减少对真实 DOM 的操作。如果没有 key,React 只能通过比较子元素的内容和顺序来判断子元素是否相同,这样会增加 React 的运算负担,降低应用程序的性能。

需要注意的是,key 值必须是唯一的,并且稳定不变的。如果列表中的 key 值发生变化,React 会认为该子元素已经被删除,而不是被更新,这样可能会导致不必要的性能损失。

因此,在使用 key 时,应该选择一个稳定不变的值,如子元素的唯一标识符或索引值。