虚拟DOM及相关「未完」

13 阅读4分钟

From DeepSeek.

核心原理

抽象表示

虚拟DOM是真实DOM的轻量级JavaScript对象表示。例如,一个<div>可能被表示为

{
    tag: 'div',
    props: { className: 'container' },
    children: [
        { tag: 'p', props: {}, children: ['Hello'] }
    ]
}

更新流程

  • 初始化: 首次渲染时,根据数据生成虚拟DOM树,并映射为真实DOM。
  • 数据变化:状态变化后,生成新的虚拟DOM树。
  • Diff算法:对比新旧虚拟DOM树的差异,如节点类型、属性、子节点变化等。
  • Patch更新:仅将差异部分应用到真实DOM。

Diff算法优化

  • 同层比较:仅比较同一层级的节点,不跨层递归;时间复杂度为O(n)。
  • Key优化:通过key标识列表元素的稳定性,减少不必要的节点销毁与重建;
  • 组件类型判断:若组件类型不同(div->span),直接替换整棵子树。

关键价值

性能优化

  • 批量更新:合并多次数据变更,避免频繁操作真实DOM。
  • 最小化变更:通过Diff算法精准定位变化,减少重排reflow和重绘repaint。

声明式编程

  • 开发者无需手动操作DOM,只需关注状态管理(如setState),框架自动处理视图更新。

跨平台能力

  • 虚拟DOM抽象了渲染逻辑,可适配不同平台(如React Native渲染原生组件、SSR服务端渲染等)。

理解误区

虚拟DOM不一定更快

  • 在简单场景下,直接操作DOM可能更高效,如一次性的单节点更新。虚拟DOM的优势体现在复杂视图的批量、智能更新。

虚拟DOM不是React专属

  • Vue、Preact等框架也基于类似思想,但具体实现细节(如Diff策略、响应式机制等)存在差异。

扩展

不使用虚拟DOM,如何手动优化DOM操作?

直接操作DOM时,需避免频繁触发重排和重绘。

批量操作DOM:减少触发次数

浏览器对DOM的修改是同步且昂贵的,批量操作可减少重排次数。

  • 文档碎片DocumentFragment
    将多次DOM插入合并为一次操作
const fragment = document.createDocumentFragment();
for (let i = 0; i < 100; i++) {
    const li = document.createElement('li');
    li.textContent = `Item ${i}`;
    fragment.appendChild(li);
}

document.getElementById('list').appendChild(fragment);
  • 离线DOM修改
    将元素移除文档流(如隐藏),完成修改后恢复。
const ele = document.getElementById('target');
ele.style.display = 'none'; // 隐藏组件,触发一次重排
// ...批量修改ele子节点
ele.style.display = 'block'; // 恢复组件,触发一次重排

避免强制同步布局(Layout Thrashing)

连续读写布局属性(如offsetWidth)会强制浏览器立即重排,导致性能骤降。

  • 读写分离:先集中读取,再批量写入。
// 错误示例:触发多次同步布局
const eles = document.querySelectorAll('.item');
eles.forEach(el => {
  const width = el.offsetWidth;
  el.style.width = width * 2 + 'px';
});

// 正确示例:先读后写
const widths = [];
eles.forEach(el => widths.push(e;.offsetWidth));
eles.forEach((el, i) => {
  el.style.width = widths[i] * 2 + 'px';
});

使用CSS类替代行内样式

通过切换CSS类名修改样式,减少逐行修改属性的开销。

// 不推荐:多次修改行内样式
ele.style.color = 'green';
ele.style.backgroundColor = 'purple';
ele.style.margin = '10px';

// 推荐:定义CSS类,一次性切换
.element-active {
  color: red;
  background-color: purple;
  margin: 10px;
}

ele.classList.add('element-active');

动画优化:利用合成层

对高频动画使用transformopacity,跳过重排与重绘,直接触发合唱(Composite)。

// 不推荐:通过top/left触发重排
.box {
  position: absolute;
  top: 0;
  left: 0;
  transition: top 0.3s, left 0.3s;
}

// 推荐:使用transform(GPU加速)
.box {
  transition: transform 0.3s;
}

.box.active {
  transform: translate(100px, 100px);
}

事件委托(Event Delegation)

减少事件监听器数量,通过父元素代理子元素事件。

// 不推荐:为每个按钮绑定事件
document.querySelectorAll('.btn').forEach(btn => {
  btn.addEventListener('click', handleClick);
});

// 推荐:通过父元素代理
document.getElementById('container').addEventListener('click', (e) => {
  if (e.target.matches('.btn') {
    handleClick(e);
  }
});

缓存DOM查询结果

避免重复查询DOM节点,存储引用以复用。

// 不推荐:多次查询同一元素
for (let i = 0; i < 100; i++) {
  document.getElementById('value').textContent = i; // 每次循环都查询
}

// 推荐:缓存引用
const valueEl = document.getElementById('value');
for (let i = 0; i < 100; i++) {
  valueEl.textContent = i;
}

手动优化 VS 虚拟DOM

场景手动优化虚拟DOM
适用性简单交互、局部更新(如表格单行修改)复杂视图、频繁数据变化(如大型表单、动态列表)
优势精细控制,无框架开销自动化批量更新,减少心智(?)负担
劣势需开发者手动管理性能,代码复杂度高存在Diff计算开销,极端场景需手动干预