function reconcile(current, target) {
if (!current ||
!target ||
current.nodeType != target.nodeType ||
current.nodeName != target.nodeName ||
current.namespaceURI != target.namespaceURI
) {
if (current && target && current.nodeName != target.nodeName) {
console.log("Cannot reconcile", current.nodeName, target.nodeName)
}
return target;
}
const hasChildren = current.firstChild || target.firstChild;
const hasAttibutes = current.hasAttributes || target.hasAttributes
if (current.nodeType === Node.TEXT_NODE) {
current.nodeValue = target.nodeValue
}
if (hasAttibutes) {
function indexAttributes(attributes) {
const index = {}
for(let i = attributes.length - 1; i >= 0; i--) {
index[attributes[i].name] = attributes[i].value;
}
return index;
}
const currentAttributes = indexAttributes(current.attributes)
const targetAttributes = indexAttributes(target.attributes)
const unionAttributeNames = new Set([...Object.keys(currentAttributes),
...Object.keys(targetAttributes)]);
for (let attributeName of unionAttributeNames) {
if (targetAttributes[attributeName]) {
if (targetAttributes[attributeName] !== currentAttributes[attributeName]) {
current.setAttribute(attributeName, targetAttributes[attributeName])
}
} else {
current.removeAttribute(attributeName);
}
}
}
for (let prop in target) {
// Events like onkeydown need to be copied over
if (prop.startsWith("on")) {
if (current[prop] !== target[prop]) {
current[prop] = target[prop];
}
}
}
// Index the children for reconciliation (if we have children)
if (hasChildren) {
function indexChildren(parent) {
const indexChildren = {}
// Collect children looking for key attribute
let index = 0;
for (let child = parent.firstChild; child; child = child.nextSibling) {
const key = child.hasAttributes && child.getAttribute("key") ?
child.getAttribute("key") : "$" + index;
indexChildren[key] = {
node: child,
index
};
index++;
}
return indexChildren;
}
const currentChildren = indexChildren(current);
const targetChildren = indexChildren(target);
// Create a new set of children, indexed by position
const newChildren = {}
// Generate reconciliation children and their ordering
const currentKeys = Object.keys(currentChildren);
const targetKeys = Object.keys(targetChildren);
const unionKeys = new Set([...currentKeys, ...targetKeys]);
for (let key of unionKeys) {
const currentChild = (currentChildren[key] || {}).node;
const targetChild = (targetChildren[key] || {}).node;
const reconciledChild = reconcile(currentChild, targetChild);
if (currentChild && reconciledChild !== currentChild) {
current.removeChild(currentChild);
}
if (reconciledChild) newChildren[targetChildren[key].index] = reconciledChild;
}
// Now we walk through the existing children,
// trying to avoid moving them if they already in right place
// This is fairly simple and probably does not scale to complex use cases
let curser = current.firstChild;
for (let i = 0; i < targetKeys.length; i++) {
if (curser === null) {
current.append(newChildren[i]);
} else {
if (curser === newChildren[i]) {
// Child is already in right place, no structural change in DOM required
curser = curser.nextSibling
} else {
// as we pruned unnecissary children already
// if there is a mismatch it probably implies the target is bigger
// If the element was in the current DOM it is moved
// If the element was in the target DOM it is added
current.insertBefore(newChildren[i], curser)
}
}
}
}
return current
}