Virtual DOM 的意义

理解 Virtual DOM 存在的原因和工作原理

问题

为什么使用 Virtual DOM?直接操作 DOM 不行吗?

解答

直接操作 DOM 的问题

DOM 操作是昂贵的。每次修改 DOM,浏览器可能需要:

  1. 重新计算样式(Recalculate Style)
  2. 重新布局(Layout/Reflow)
  3. 重新绘制(Paint)
  4. 合成图层(Composite)
// 糟糕的做法:频繁操作 DOM
for (let i = 0; i < 1000; i++) {
  document.body.innerHTML += `<div>${i}</div>`; // 每次都触发重排重绘
}

// 稍好的做法:批量操作
const fragment = document.createDocumentFragment();
for (let i = 0; i < 1000; i++) {
  const div = document.createElement('div');
  div.textContent = i;
  fragment.appendChild(div);
}
document.body.appendChild(fragment); // 只触发一次

Virtual DOM 是什么

Virtual DOM 是用 JavaScript 对象描述 DOM 结构:

// 真实 DOM
// <div class="container">
//   <span>Hello</span>
// </div>

// Virtual DOM
const vnode = {
  tag: 'div',
  props: { class: 'container' },
  children: [
    {
      tag: 'span',
      props: null,
      children: ['Hello']
    }
  ]
};

Virtual DOM 的工作流程

// 简化的 Virtual DOM 实现
function createElement(tag, props, ...children) {
  return { tag, props, children: children.flat() };
}

// 渲染 Virtual DOM 到真实 DOM
function render(vnode) {
  // 文本节点
  if (typeof vnode === 'string' || typeof vnode === 'number') {
    return document.createTextNode(vnode);
  }

  const el = document.createElement(vnode.tag);

  // 设置属性
  if (vnode.props) {
    Object.entries(vnode.props).forEach(([key, value]) => {
      el.setAttribute(key, value);
    });
  }

  // 递归渲染子节点
  vnode.children.forEach(child => {
    el.appendChild(render(child));
  });

  return el;
}

// 使用
const vdom = createElement('div', { class: 'app' },
  createElement('h1', null, 'Title'),
  createElement('p', null, 'Content')
);

document.body.appendChild(render(vdom));

Diff 算法

Virtual DOM 的价值在于 diff:比较新旧两棵树,找出最小变更:

// 简化的 diff 实现
function diff(oldVNode, newVNode, parent, index = 0) {
  const el = parent.childNodes[index];

  // 新节点不存在,删除旧节点
  if (!newVNode) {
    parent.removeChild(el);
    return;
  }

  // 旧节点不存在,添加新节点
  if (!oldVNode) {
    parent.appendChild(render(newVNode));
    return;
  }

  // 节点类型不同,直接替换
  if (oldVNode.tag !== newVNode.tag) {
    parent.replaceChild(render(newVNode), el);
    return;
  }

  // 文本节点,更新内容
  if (typeof newVNode === 'string') {
    if (oldVNode !== newVNode) {
      el.textContent = newVNode;
    }
    return;
  }

  // 更新属性
  updateProps(el, oldVNode.props, newVNode.props);

  // 递归比较子节点
  const maxLen = Math.max(
    oldVNode.children.length,
    newVNode.children.length
  );
  for (let i = 0; i < maxLen; i++) {
    diff(oldVNode.children[i], newVNode.children[i], el, i);
  }
}

function updateProps(el, oldProps = {}, newProps = {}) {
  // 移除旧属性
  Object.keys(oldProps).forEach(key => {
    if (!(key in newProps)) {
      el.removeAttribute(key);
    }
  });
  // 设置新属性
  Object.keys(newProps).forEach(key => {
    if (oldProps[key] !== newProps[key]) {
      el.setAttribute(key, newProps[key]);
    }
  });
}

Virtual DOM 的真正价值

// 假设有 1000 个列表项,只有第 500 个变了
const oldList = Array.from({ length: 1000 }, (_, i) => ({
  tag: 'li',
  props: null,
  children: [`Item ${i}`]
}));

const newList = [...oldList];
newList[500] = { tag: 'li', props: null, children: ['Updated Item'] };

// Virtual DOM diff 后,只会更新第 500 个 <li>
// 而不是重新渲染整个列表

关键点

  • 批量更新:Virtual DOM 收集多次状态变更,一次性计算出最小 DOM 操作
  • 跨平台:同一套 Virtual DOM 可以渲染到 DOM、Canvas、Native 等不同平台
  • 声明式编程:开发者只需描述 UI 应该是什么样,框架负责计算如何更新
  • 性能并非最优:手动精细操作 DOM 可能更快,但 Virtual DOM 提供了性能与开发体验的平衡
  • diff 有成本:Virtual DOM 本身的创建和 diff 也消耗性能,对于简单场景可能是负优化