当前位置: 首页
前端开发
虚拟DOM中间层的智慧解析

虚拟DOM中间层的智慧解析

热心网友 时间:2026-06-15
转载

虚拟DOM用JS对象描述真实DOM,降低操作成本。其Diff算法采用同层比较,时间复杂度降至O(n),结合key标识节点实现最小化移动替换。虽然单个更新不如直接操作DOM快,但批量更新自动合并,提供声明式编程体验和跨平台能力。

操作真实 DOM 有多昂贵?

先来看一段简单的示例代码:

虚拟 DOM —— 中间层的智慧

// 将一个 

的背景色更改为红色 document.getElementById('box').style.backgroundColor = 'red'

你是否想过,执行这一行代码背后究竟耗费了多少资源?

实际情况远比表面复杂:

1. JS 引擎首先定位到对应的 DOM 节点
2. 修改该节点的 style 属性
3. 浏览器标记该节点需要重新计算样式(Recalculate Style)
4. 触发重新布局(Layout/Reflow)—— 可能影响相邻元素的位置
5. 执行重新绘制(Paint)—— 将新的颜色渲染到屏幕上
6. 最后进行合成(Composite)—— 将各层合并为最终画面

请注意,仅仅修改一个属性就可能引发整个渲染流水线的连锁反应。如果同时修改 1000 个属性呢?若一次操作需要新增、删除或移动数百个节点,浏览器的工作负载将不堪重负。


什么是虚拟 DOM?

虚拟 DOM 本质上是用一个普通的 Ja vaScript 对象来描述一个 DOM 节点结构。

例如,下面这段真实 DOM:

Hello

对应的虚拟 DOM 表示为:

{
  tag: 'div',
  props: { id: 'app', class: 'container' },
  children: [
    {
      tag: 'p',
      props: {},
      children: [
        { tag: undefined, text: 'Hello' }  // 文本节点
      ]
    }
  ]
}

在 Vue 中,这个 JS 对象被称为 VNode(Virtual Node)


为何不使用真实 DOM,而要自己构建一个抽象层?

一句话概括:真实 DOM 虽然功能完备,但过于笨重,操作成本极高。让我们通过对比来看差距:

真实 DOM虚拟 DOM (VNode)
本质C++ 实现的浏览器对象普通 JS 对象
创建成本高(需创建数百个属性)低(仅几个字段)
操作成本高(可能触发回流重排)低(仅修改 JS 对象)
跨平台能力仅限浏览器环境可渲染到不同平台
可控性由浏览器引擎决定由框架完全掌控

核心思想很直观:将计算任务放在 JS 层面完成,最后只执行一次最小化的真实 DOM 更新。这笔投入回报比非常高。


一个形象的比喻:装修房间

想象你正在重新装修一间屋子,有两种方案可以选择:

方案一:直接动手操作(直接操作 DOM)

"把左边这面墙拆掉"    → 工人立刻开始拆
"等等,右边那面也拆"  → 工人换位置继续拆
"不对,左边还是保留吧" → 工人:???

每一条指令都立即执行,一旦改变主意就得返工,工人疲惫、工期延长、成本高昂。

方案二:先在图纸上规划(虚拟 DOM)

在图纸上绘制一遍 → 比对新旧图纸 → 标记所有变动 → 一次性施工完成

先在纸上(JS 内存)将所有方案推演完毕,确认无误后,列出最小改动清单,交给工人一次性施工。既节省费用又省心省力。


Diff 算法:如何找出最小改动?

现在,我们有一棵旧 VNode 树和一棵新 VNode 树。如何找到“最少改动”的路径?

如果对两棵树进行完全比较,时间复杂度是 O(n³)。这意味着什么?一棵包含 1000 个节点的树,需要执行 10 亿次比较,这是完全不可接受的。

但前端开发的实际场景为我们提供了一个关键观察:大多数 UI 更新中,跨层级的节点移动非常罕见。几乎所有改动都发生在同一层级内部。

基于这一洞察,Vue(以及 React)的 diff 算法做出了一个大胆的简化:只进行同层比较,跳过跨层级比较。这样一来,算法复杂度直接从 O(n³) 降低到 O(n)——每个节点只需要比较一次,性价比极高。


同层 Diff 的三个关键步骤

Vue 的 diff 采用了双端比较策略。下面以子节点数组的 diff 为例,看看具体如何操作。

假设旧子节点顺序为 [A, B, C, D],新子节点顺序为 [B, A, D, E]

步骤 1:头头比较

旧: [A, B, C, D]
     ↑
新: [B, A, D, E]
     ↑
A !== B → 不匹配,结束头头比较

步骤 2:尾尾比较

旧: [A, B, C, D]
              ↑
新: [B, A, D, E]
              ↑
D !== E → 不匹配,结束尾尾比较

步骤 3:头尾交叉比较

旧头 vs 新尾: A vs E → 不匹配
旧尾 vs 新头: D vs B → 不匹配

四个指针全部匹配失败,说明这一轮变化比较复杂,需要更高级的策略。

如果设置了 key

key 的作用是为每个 VNode 提供一个唯一的身份标识。有了它,diff 算法就能识别出“这个节点只是位置发生了变化,而不是被删除并重建”。

旧: [{key:'A'}, {key:'B'}, {key:'C'}, {key:'D'}]
新: [{key:'B'}, {key:'A'}, {key:'D'}, {key:'E'}]

使用 key 时:
  B 在旧节点中找到 → 只需移动位置
  A 在旧节点中找到 → 只需移动位置
  D 在旧节点中找到 → 只需移动位置
  E 不在旧节点中 → 需要新建

不使用 key 时:
  可能错误地将 B 当作 A(因为两者都在第一个位置)
  → 更新 A 的内容为 B,而不是移动
  → 效率低下,还可能导致组件状态丢失

这就是为什么在 v-for 中总是要求提供稳定的 key。这并非框架矫情,而是它确实能让 diff 算法少走很多弯路。


动手实现一个迷你 VNode + Diff

创建 VNode

function createVNode(tag, props, children) {
  return { tag, props, children }
}

function h(tag, props, ...children) {
  return createVNode(tag, props, children.flat())
}

将 VNode 渲染为真实 DOM

function mount(vnode, container) {
  // 创建对应元素
  const el = document.createElement(vnode.tag)
  
  // 设置属性
  if (vnode.props) {
    for (const key in vnode.props) {
      el.setAttribute(key, vnode.props[key])
    }
  }
  
  // 处理子节点
  if (vnode.children) {
    vnode.children.forEach(child => {
      if (typeof child === 'string') {
        el.appendChild(document.createTextNode(child))
      } else {
        mount(child, el)  // 递归挂载
      }
    })
  }
  
  container.appendChild(el)
  vnode.el = el  // 保存对真实 DOM 的引用
}

Diff 和 Patch 实现

function patch(oldVNode, newVNode) {
  const el = (newVNode.el = oldVNode.el)
  
  // 1. 标签不同 → 直接替换
  if (oldVNode.tag !== newVNode.tag) {
    const newEl = document.createElement(newVNode.tag)
    el.parentNode.replaceChild(newEl, el)
    mount(newVNode, el.parentNode)
    return
  }
  
  // 2. 更新属性
  // 移除旧属性
  for (const key in oldVNode.props) {
    if (!(key in newVNode.props)) {
      el.removeAttribute(key)
    }
  }
  
  // 设置新属性
  for (const key in newVNode.props) {
    if (oldVNode.props[key] !== newVNode.props[key]) {
      el.setAttribute(key, newVNode.props[key])
    }
  }
  
  // 3. 更新子节点
  const oldChildren = oldVNode.children || []
  const newChildren = newVNode.children || []
  const len = Math.max(oldChildren.length, newChildren.length)
  
  for (let i = 0; i < len; i++) {
    if (i >= oldChildren.length) {
      // 新节点,直接挂载
      mount(newChildren[i], el)
    } else if (i >= newChildren.length) {
      // 旧节点多余,删除
      el.removeChild(oldChildren[i].el)
    } else {
      // 都存在,递归 patch
      if (typeof oldChildren[i] === 'string' && typeof newChildren[i] === 'string') {
        if (oldChildren[i] !== newChildren[i]) {
          el.childNodes[i].textContent = newChildren[i]
        }
      } else {
        patch(oldChildren[i], newChildren[i])
      }
    }
  }
}

上面的实现省略了 key 的匹配逻辑,已经相当精炼,但它清晰展示了 Diff 的核心思想:同层比较,最小化 DOM 操作


虚拟 DOM vs 直接操作 DOM:究竟谁更快?

这是一个经典问题。实际上,没有绝对的快慢,只有更匹配的场景:

场景直接操作 DOM虚拟 DOM
单个更新 更快 有 diff 开销
批量更新 需要手动优化 自动合并
代码可维护性 逻辑分散在各处 声明式编程
跨平台能力 仅浏览器 可渲染到原生环境

虚拟 DOM 从来不是为了“比直接操作 DOM 更快”而设计的。它的真正价值在于提供一种声明式的编程体验,同时保持“足够好”的性能。在绝大多数应用场景下,这个权衡非常明智。


总结

  1. 为什么需要虚拟 DOM:真实 DOM 操作成本高昂,虚拟 DOM 在 JS 层完成计算,最后一次性最少地更新真实 DOM。
  2. VNode:用 JS 对象描述 DOM 节点,创建和比较成本极低。
  3. Diff 算法:同层比较,O(n) 复杂度。双端比较 + key 优化是最核心的策略。
  4. Key 的作用:为节点提供唯一标识,让 Diff 能区分“移动”和“替换”。
  5. 性能本质:虚拟 DOM 是“足够快 + 易于维护”的平衡方案。

有了虚拟 DOM,Vue 就能知道“视图应该呈现什么样的结构”。但视图是由组件构成的——组件是如何创建、挂载、更新的?这将是下一个我们要探讨的话题。

来源:https://juejin.cn/post/7650083635420725298

游乐网为非赢利性网站,所展示的游戏/软件/文章内容均来自于互联网或第三方用户上传分享,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系youleyoucom@outlook.com。

同类文章
更多
如何用拖放在Flask看板应用中更新数据库状态

如何用拖放在Flask看板应用中更新数据库状态

在Flask看板应用中,通过HTML5拖放API结合前端fetch异步POST请求,将拖拽任务的task_id和target_state发送至后端路由;后端接收JSON后更新数据库并提交事务,实现跨列拖拽时实时更新状态。需注意将事件绑定到容器而非子元素,并在ondragover中阻止默认行为以确保drop触发。

时间:2026-07-09 07:01
如何用CSS :has()选择器快速实现悬停span显示相邻div

如何用CSS :has()选择器快速实现悬停span显示相邻div

利用CSS的:has()伪类选择器,通过` bar:has(> baz:hover)+ qux`实现悬停内联元素时显示相邻块级容器,解决了传统相邻兄弟选择器无法跨层级匹配的局限,可用于悬停图标显示详情框等交互场景。需注意浏览器兼容性和性能优化,避免过度使用影响渲染效率。

时间:2026-07-09 07:01
display:block居中段落文本的响应式实现方法

display:block居中段落文本的响应式实现方法

通过`display:block`与`max-width`限制宽度,配合`margin:auto`使段落块水平居中并实现响应式布局;同时用CSS统一控制段落间距,避免脱离文档流,提升可读性与维护性。

时间:2026-07-09 07:01
JavaScript代码去重:高效避免重复编写的技巧

JavaScript代码去重:高效避免重复编写的技巧

将重复代码中的变化点提取为参数,利用ES6模板字符串动态生成选择器,可消除重复、保持一致性。注意参数安全性,必要时进行白名单校验;批量化场景可扩展为数组形式;现代框架中更推荐数据驱动或组件化封装替代手动选择器。

时间:2026-07-09 07:00
使用容器查询单位cqw实现表单输入框相对于祖父容器百分比宽度

使用容器查询单位cqw实现表单输入框相对于祖父容器百分比宽度

利用CSS容器查询单位cqw,表单输入框宽度可直接基于祖父容器计算,绕过中间包装器尺寸干扰,实现简洁响应式布局,无需硬编码或JavaScript,显著降低维护成本,提升嵌套结构下的布局灵活性。

时间:2026-07-09 07:00
热门专题
更多
刀塔传奇破解版无限钻石下载大全 刀塔传奇破解版无限钻石下载大全
洛克王国正式正版手游下载安装大全 洛克王国正式正版手游下载安装大全
思美人手游下载专区 思美人手游下载专区
好玩的阿拉德之怒游戏下载合集 好玩的阿拉德之怒游戏下载合集
不思议迷宫手游下载合集 不思议迷宫手游下载合集
百宝袋汉化组游戏最新合集 百宝袋汉化组游戏最新合集
jsk游戏合集30款游戏大全 jsk游戏合集30款游戏大全
宾果消消消原版下载大全 宾果消消消原版下载大全
  • 热门数据榜