VDOM

261 阅读3分钟

何为VDOM

虚拟DOM(Virtual Document Object Model)是指由JavaScript对象构成的一棵“树”,它可以表示一个DOM的层次结构,并且可以轻松地被修改、复制和比较。

在传统的页面渲染中,当页面元素发生变化时,浏览器会重新构建整个DOM树,并且重新渲染整个页面。这个过程非常耗费性能,尤其是在页面元素很多的情况下,会造成明显的页面卡顿和延迟。虚拟DOM的提出,就是为了解决这个问题。

虚拟DOM采用了一种巧妙的渲染方式,它先将整个DOM树以JavaScript对象的形式存放在内存中,然后对这个“虚拟DOM”进行操作,最后再将其转换为实际的DOM树,从而实现页面渲染。这种方式可以避免频繁地进行DOM操作,从而提高了页面渲染的效率。

虚拟DOM还具有另一个重要的优点,就是支持组件化开发。在虚拟DOM中,每个组件都是一个独立的虚拟DOM节点,组件之间可以方便地传递数据和事件。这种方式让开发者更加专注于组件的实现,从而提高了代码的可重用性和维护性

虚拟DOM的实现

创建虚拟DOM节点:

虚拟DOM节点一般用JavaScript对象来表示。例如:

const element = {
  type: "div",
  props: {
    className: "foo",
    children: [{ type: "p", props: { children: "Hello, world!" } }],
  },
};

这里创建了一个div节点,并且他的props属性包括一个className,以及一个包含一个p节点的children属性数组。

将虚拟DOM节点转换为真实的DOM节点:

将虚拟DOM节点转换为真实的DOM节点需要利用document.createElement()函数来实现。例如:

function createElement(node) {
  if (typeof node === "string") {
    return document.createTextNode(node);
  }
  const element = document.createElement(node.type);
  node.props.children.forEach((child) => {
    element.appendChild(createElement(child));
  });
  return element;
}
const rootElement = createElement(element);

这里用createElement()函数将虚拟DOM节点转换成了真实的DOM节点。

比较新旧虚拟DOM节点:

比较新旧虚拟DOM节点,找出差异,并将差异保存下来。比较可以采用深度优先遍历的方式,逐一比较节点的类型、属性和子节点。例如:

function diff(oldNode, newNode) {
  if (oldNode.type !== newNode.type) {
    return {
      type: 'NODE_TYPE_CHANGED',
      payload: newNode
    };
  }
  if (typeof oldNode !== typeof newNode) {
    return {
      type: 'NODE_CHANGED',
      payload: newNode
    };
  }
  if (typeof oldNode === 'string' && oldNode !== newNode) {
    return {
      type: 'NODE_CHANGED',
      payload: newNode
    };
  }
  const propsDiff = diffProps(oldNode.props, newNode.props);
  const childrenDiff = diffChildren(oldNode.props.children, newNode.props.children);
  if (propsDiff || childrenDiff) {
    return {
      type: 'NODE_PROPS_CHANGED',
      payload: {
        props: propsDiff,
        children: childrenDiff
      }
    }
  }
  return null;
}
function diffProps(oldProps, newProps) {
  // 比较props属性
}
function diffChildren(oldChildren, newChildren) {
  // 比较子节点
}

根据差异更新真实的DOM节点:

根据前面比较出来的差异信息,对实际的DOM节点进行更新。例如:

function patch(node, patches) {
  // 根据差异信息更新节点
  patches.forEach((patch) => {
    if (patch.type === "NODE_TYPE_CHANGED") {
      const newNode = createElement(patch.payload);
      node.parentNode.replaceChild(newNode, node);
    }
    if (patch.type === "NODE_CHANGED") {
      node.nodeValue = patch.payload;
    }
    if (patch.type === "NODE_PROPS_CHANGED") {
      patch.props.forEach((propPatch) => {
        if (propPatch.type === "SET_PROP") {
          node.setAttribute(propPatch.key, propPatch.value);
        }
        if (propPatch.type === "REMOVE_PROP") {
          node.removeAttribute(propPatch.key);
        }
      });
      patch.children.forEach((childPatch) => {
        patch(node.childNodes[childPatch.index], childPatch);
      });
    }
  });
}
const patches = diff(oldNode, newNode);
patch(rootElement, patches);

这里用patch()函数根据差异信息更新实际的DOM节点。 以上是虚拟DOM的基本实现步骤,实际的实现可能还需要考虑一些特殊情况,例如组件的状态更新、事件处理等。