Understanding Vue's Virtual DOM and Diffing Algorithm

Vue's reactivity system triggers view updates when data changes. Directly manipulating the actual DOM is expensive because browser DOM implementations are inherently heavy and complex objects. Consider the following inspection of a simple div element:

const element = document.createElement('div');
let properties = '';
for (let prop in element) {
  properties += prop + ' ';
}
console.log(properties);

To mitigate this, Vue trades computational cost for DOM manipulation cost using a Virtual DOM (VNode) layer.

The Diffing Strategy (patch)

The core update mechanism, often caled patch or DOM-Diff, handles three primary scenarios:

  • Creation: If a node exists in the new VNode tree but not in the old one, it is created and inserted.
  • Deletion: If a node exists in the old tree but not in the new one, it is removed.
  • Update: If a node exists in both, its updated to match the new state.

Optimizing Child Node Updates

Vue optimizes child list comparison by using a two-pointer approach (start and end indices) rather than a naive double loop. This reduces the time complexity in common cases.

function reconcileChildren(parentNode, oldList, newList, queue, onlyRemove) {
  let oldStart = 0;
  let newStart = 0;
  let oldEnd = oldList.length - 1;
  let newEnd = newList.length - 1;
  
  let oldStartNode = oldList[0];
  let oldEndNode = oldList[oldEnd];
  let newStartNode = newList[0];
  let newEndNode = newList[newEnd];
  
  let keyMap, indexInOld, targetNode, referenceEl;
  const allowMove = !onlyRemove;

  while (oldStart <= oldEnd && newStart <= newEnd) {
    if (isUndef(oldStartNode)) {
      oldStartNode = oldList[++oldStart];
    } else if (isUndef(oldEndNode)) {
      oldEndNode = oldList[--oldEnd];
    } else if (isSameNode(oldStartNode, newStartNode)) {
      // Case 1: Head-Head match
      updateNode(oldStartNode, newStartNode, queue, newList, newStart);
      oldStartNode = oldList[++oldStart];
      newStartNode = newList[++newStart];
    } else if (isSameNode(oldEndNode, newEndNode)) {
      // Case 2: Tail-Tail match
      updateNode(oldEndNode, newEndNode, queue, newList, newEnd);
      oldEndNode = oldList[--oldEnd];
      newEndNode = newList[--newEnd];
    } else if (isSameNode(oldStartNode, newEndNode)) {
      // Case 3: Head-Tail match (Node moved right)
      updateNode(oldStartNode, newEndNode, queue, newList, newEnd);
      allowMove && insertBefore(parentNode, oldStartNode.elm, nextSibling(oldEndNode.elm));
      oldStartNode = oldList[++oldStart];
      newEndNode = newList[--newEnd];
    } else if (isSameNode(oldEndNode, newStartNode)) {
      // Case 4: Tail-Head match (Node moved left)
      updateNode(oldEndNode, newStartNode, queue, newList, newStart);
      allowMove && insertBefore(parentNode, oldEndNode.elm, oldStartNode.elm);
      oldEndNode = oldList[--oldEnd];
      newStartNode = newList[++newStart];
    } else {
      // Fallback: Look for keyed node in remaining old list
      if (isUndef(keyMap)) keyMap = mapOldNodes(oldList, oldStart, oldEnd);
      
      indexInOld = isDef(newStartNode.key) 
        ? keyMap[newStartNode.key] 
        : findPosition(newStartNode, oldList, oldStart, oldEnd);

      if (isUndef(indexInOld)) {
        // New node, create and insert
        createNewNode(newStartNode, queue, parentNode, oldStartNode.elm, false, newList, newStart);
      } else {
        targetNode = oldList[indexInOld];
        if (isSameNode(targetNode, newStartNode)) {
          updateNode(targetNode, newStartNode, queue, newList, newStart);
          oldList[indexInOld] = undefined;
          allowMove && insertBefore(parentNode, targetNode.elm, oldStartNode.elm);
        } else {
          createNewNode(newStartNode, queue, parentNode, oldStartNode.elm, false, newList, newStart);
        }
      }
      newStartNode = newList[++newStart];
    }
  }

  // Handle remaining nodes
  if (oldStart > oldEnd) {
    referenceEl = isUndef(newList[newEnd + 1]) ? null : newList[newEnd + 1].elm;
    addNodes(parentNode, referenceEl, newList, newStart, newEnd, queue);
  } else if (newStart > newEnd) {
    removeNodes(oldList, oldStart, oldEnd);
  }
}

Step-by-Step Logic

  1. Skip Invalid Nodes: If oldStartNode or oldEndNode is undefined (already processed), skip them by adjusting pointers.
  2. Head-to-Head: If the start of the old list matches the start of the new list, update the node and move both start pointers forward.
  3. Tail-to-Tail: If the end of the old list matches the end of the new list, update the node and move both end pointers backward.
  4. Head-to-Tail (Move Right): If the start of the old list matches the end of the new list, update the node and move the old start to the end of the processed section (right), then adjust pointers.
  5. Tail-to-Head (Move Left): If the end of the old list matches the start of the new list, update the node and move the old end to the beginning of the processed section (left), then adjust pointers.
  6. Keyed Lookup: If none of the above match, search the remaining old nodes (using keys if available) to find a match. If found, update and move; if not, create a new node.
  7. Cleanup:
    • If old pointers cross (old list exhausted), add remaining new nodes.
    • If new pointers cross (new list exhausted), remove remaining old nodes.

Tags: Vue.js Virtual DOM source code analysis Diff Algorithm javascript

Posted on Tue, 01 Sep 2026 16:51:05 +0000 by ChaosDream