虚拟Dom以及Diff算法

176 阅读3分钟

实现一个简易的虚拟DOM

实现一个简易的虚拟 DOM 需要分三步走:

  1. 用 JS 对象描述 DOM 结构
  2. 对比新旧虚拟 DOM 树的差异
  3. 将差异批量更新到真实 DOM

以下是分步说明及核心代码示例。


一、用 JS 对象描述 DOM 结构(Virtual Node)

// 定义虚拟节点类型
function createElement(type, props, ...children) {
  return {
    type, // 标签名(如 'div')
    props, // 属性(含 children)
    key: props?.key || null, // 唯一标识(用于 diff 优化)
    children: children.flat().map(child =>
      typeof child === 'object' ? child : createTextVNode(child)
    )
  };
}

// 处理文本节点
function createTextVNode(text) {
  return {
    type: 'TEXT_ELEMENT',
    props: { nodeValue: text, children: [] }
  };
}

// 使用示例
const virtualDOM = createElement('div', { id: 'app' },
  createElement('h1', { style: 'color: red' }, 'Hello'),
  createElement('p', null, 'Virtual DOM Demo')
);

二、差异对比算法(Diffing)

核心策略:

  • 同层比较
  • 组件级跳过
  • 元素级 Key 匹配
function diff(oldVNode, newVNode) {
  const patches = []; // 存储差异补丁
  dfsDiff(oldVNode, newVNode, 0, patches);
  return patches;
}

function dfsDiff(oldNode, newNode, index, patches) {
  if (!newNode) {
    patches.push({ type: 'REMOVE', index });
    return;
  }

  if (isSameNodeType(oldNode, newNode)) {
    // 属性差异对比
    const propsPatches = diffProps(oldNode.props, newNode.props);
    if (propsPatches.length) {
      patches.push({ type: 'PROPS', index, props: propsPatches });
    }

    // 递归对比子节点
    diffChildren(oldNode.children, newNode.children, patches);
  } else {
    patches.push({ type: 'REPLACE', newNode });
  }
}

function diffProps(oldProps, newProps) {
  const patches = [];
  // 对比样式、class、事件等属性变化...
  return patches;
}

function diffChildren(oldChildren, newChildren, patches) {
  // 实现双端比较或最长递增子序列算法(此处简化)
  const diff = listDiff(oldChildren, newChildren, 'key');
  diff.forEach(patch => {
    const index = patch.index;
    if (patch.type === 'INSERT') {
      patches.push({ type: 'INSERT', newNode: patch.node, index });
    } else if (patch.type === 'MOVE') {
      patches.push({ type: 'MOVE', from: patch.from, to: index });
    }
  });
}

三、应用差异到真实 DOM(Patching)

function patch(node, patches) {
  const walker = { index: 0 };
  dfsWalk(node, walker, patches);
}

function dfsWalk(node, walker, patches) {
  const currentPatches = patches[walker.index];

  // 应用属性补丁
  currentPatches?.props.forEach(propPatch => {
    applyPropPatch(node, propPatch);
  });

  // 应用子节点补丁
  let childIndex = 0;
  const children = Array.isArray(node.childNodes) ? node.childNodes : [];

  while (childIndex < children.length) {
    dfsWalk(children[childIndex], walker, patches);
    childIndex++;
  }

  // 应用替换/插入/移动操作
  currentPatches?.forEach(patch => {
    switch (patch.type) {
      case 'REPLACE':
        const newNode = patch.newNode.type === 'TEXT_ELEMENT'
          ? document.createTextNode(patch.newNode.props.nodeValue)
          : document.createElement(patch.newNode.type);
        node.parentNode.replaceChild(newNode, node);
        break;

      case 'INSERT':
        const insertNode = createRealNode(patch.newNode);
        node.insertBefore(insertNode, node.childNodes[patch.index] || null);
        break;

      case 'MOVE':
        const movingNode = node.removeChild(node.childNodes[patch.from]);
        node.insertBefore(movingNode, node.childNodes[patch.to] || null);
        break;

      case 'REMOVE':
        node.removeChild(node.childNodes[patch.index]);
        break;
    }
  });
}

四、关键优化策略

1. Key 的妙用

// 列表对比时通过 key 快速定位节点
function listDiff(oldList, newList, key) {
  const keyMap = new Map();
  newList.forEach(node => {
    const nodeKey = node.props[key];
    if (nodeKey !== undefined) keyMap.set(nodeKey, node);
  });

  // 实现移动/插入/删除逻辑...
}

2. 批量更新策略

// 使用 requestIdleCallback 实现异步更新
function scheduleUpdate(patches) {
  requestIdleCallback(() => {
    applyPatches(patches);
  });
}

3. 事件委托优化

// 在 patch 阶段自动绑定事件监听
function applyPropPatch(node, propPatch) {
  if (propPatch.type === 'EVENT') {
    node.addEventListener(propPatch.name, propPatch.handler);
  }
}

五、完整使用示例

// 1. 首次渲染
const realDOM = render(virtualDOM);
document.body.appendChild(realDOM);

// 2. 更新虚拟DOM
const newVirtualDOM = createElement('div', { id: 'app' },
  createElement('h1', { style: 'color: blue' }, 'Hello'),
  createElement('p', { key: 'p' }, 'Updated Content')
);

// 3. 计算差异并打补丁
const patches = diff(virtualDOM, newVirtualDOM);
patch(realDOM, patches);

六、实现要点总结

  1. 分层对比:通过深度优先遍历逐层比较,避免全局遍历
  2. 组件隔离:遇到组件边界时直接跳过子树对比
  3. 移动优化:对列表操作使用 LIS(最长递增子序列)算法减少 DOM 操作
  4. 批处理机制:将多个更新合并为一次 DOM 操作

实际框架(如 React)的实现复杂度更高,包含:

  • Fiber 架构实现可中断的异步渲染
  • 优先级调度系统
  • 并发模式(Concurrent Mode)
  • 更精细的副作用管理

这个简化实现能帮助理解核心原理,但要用于生产环境还需处理边界条件、添加类型系统、优化性能等。