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
- Skip Invalid Nodes: If
oldStartNodeoroldEndNodeis undefined (already processed), skip them by adjusting pointers. - 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.
- 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.
- 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.
- 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.
- 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.
- Cleanup:
- If old pointers cross (old list exhausted), add remaining new nodes.
- If new pointers cross (new list exhausted), remove remaining old nodes.