Skip to content

Commit

Permalink
Extract non-element morphs for performance
Browse files Browse the repository at this point in the history
  • Loading branch information
joeldrapper committed Mar 10, 2024
1 parent 1686eea commit e83be73
Show file tree
Hide file tree
Showing 2 changed files with 117 additions and 88 deletions.
101 changes: 58 additions & 43 deletions dist/morphlex.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

104 changes: 59 additions & 45 deletions src/morphlex.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,21 +146,35 @@ class Morph {
}

// This is where we actually morph the nodes. The `morph` function (above) exists only to set up the `idMap`.
#morphNode(node: ChildNode, ref: ReadonlyNode<ChildNode>): void {
if (!(this.#options.beforeNodeMorphed?.(node, ref as ChildNode) ?? true)) return;

if (isElement(node) && isElement(ref) && node.localName === ref.localName) {
if (node.hasAttributes() || ref.hasAttributes()) this.#morphAttributes(node, ref);
if (isHead(node)) this.#morphHead(node, ref as ReadonlyNode<HTMLHeadElement>);
else if (node.hasChildNodes() || ref.hasChildNodes()) this.#morphChildNodes(node, ref);
#morphNode(node: ChildNode, reference: ReadonlyNode<ChildNode>): void {
if (isElement(node) && isElement(reference) && node.localName === reference.localName) {
this.#morphMatchingElementNode(node, reference);
} else {
if (node.nodeType === ref.nodeType && node.nodeValue !== null && ref.nodeValue !== null) {
// Handle text nodes, comments, and CDATA sections.
this.#updateProperty(node, "nodeValue", ref.nodeValue);
} else this.#replaceNode(node, ref.cloneNode(true));
this.#morphOtherNode(node, reference);
}
}

#morphMatchingElementNode(node: Element, reference: ReadonlyNode<Element>): void {
if (!(this.#options.beforeNodeMorphed?.(node, reference as ChildNode) ?? true)) return;

if (node.hasAttributes() || reference.hasAttributes()) this.#morphAttributes(node, reference);

if (isHead(node)) {
this.#morphHead(node, reference as ReadonlyNode<HTMLHeadElement>);
} else if (node.hasChildNodes() || reference.hasChildNodes()) this.#morphChildNodes(node, reference);

this.#options.afterNodeMorphed?.(node, reference as ChildNode);
}

#morphOtherNode(node: ChildNode, reference: ReadonlyNode<ChildNode>): void {
if (!(this.#options.beforeNodeMorphed?.(node, reference as ChildNode) ?? true)) return;

if (node.nodeType === reference.nodeType && node.nodeValue !== null && reference.nodeValue !== null) {
// Handle text nodes, comments, and CDATA sections.
this.#updateProperty(node, "nodeValue", reference.nodeValue);
} else this.#replaceNode(node, reference.cloneNode(true));

this.#options.afterNodeMorphed?.(node, ref as ChildNode);
this.#options.afterNodeMorphed?.(node, reference as ChildNode);
}

#morphHead(node: HTMLHeadElement, reference: ReadonlyNode<HTMLHeadElement>): void {
Expand All @@ -182,17 +196,17 @@ class Morph {
for (const refChild of refChildNodesMap.values()) this.#appendChild(node, refChild.cloneNode(true));
}

#morphAttributes(element: Element, ref: ReadonlyNode<Element>): void {
#morphAttributes(element: Element, reference: ReadonlyNode<Element>): void {
// Remove any excess attributes from the element that aren’t present in the reference.
for (const { name, value } of element.attributes) {
if (!ref.hasAttribute(name) && (this.#options.beforeAttributeUpdated?.(element, name, null) ?? true)) {
if (!reference.hasAttribute(name) && (this.#options.beforeAttributeUpdated?.(element, name, null) ?? true)) {
element.removeAttribute(name);
this.#options.afterAttributeUpdated?.(element, name, value);
}
}

// Copy attributes from the reference to the element, if they don’t already match.
for (const { name, value } of ref.attributes) {
for (const { name, value } of reference.attributes) {
const previousValue = element.getAttribute(name);
if (previousValue !== value && (this.#options.beforeAttributeUpdated?.(element, name, value) ?? true)) {
element.setAttribute(name, value);
Expand All @@ -202,46 +216,47 @@ class Morph {

// For certain types of elements, we need to do some extra work to ensure
// the element’s state matches the reference elements’ state.
if (isInput(element) && isInput(ref)) {
this.#updateProperty(element, "checked", ref.checked);
this.#updateProperty(element, "disabled", ref.disabled);
this.#updateProperty(element, "indeterminate", ref.indeterminate);
if (isInput(element) && isInput(reference)) {
this.#updateProperty(element, "checked", reference.checked);
this.#updateProperty(element, "disabled", reference.disabled);
this.#updateProperty(element, "indeterminate", reference.indeterminate);
if (
element.type !== "file" &&
!(this.#options.ignoreActiveValue && document.activeElement === element) &&
!(this.#options.preserveModifiedValues && element.name === ref.name && element.value !== element.defaultValue)
!(this.#options.preserveModifiedValues && element.name === reference.name && element.value !== element.defaultValue)
) {
this.#updateProperty(element, "value", ref.value);
this.#updateProperty(element, "value", reference.value);
}
} else if (isOption(element) && isOption(ref)) {
this.#updateProperty(element, "selected", ref.selected);
} else if (isOption(element) && isOption(reference)) {
this.#updateProperty(element, "selected", reference.selected);
} else if (
isTextArea(element) &&
isTextArea(ref) &&
isTextArea(reference) &&
!(this.#options.ignoreActiveValue && document.activeElement === element) &&
!(this.#options.preserveModifiedValues && element.name === ref.name && element.value !== element.defaultValue)
!(this.#options.preserveModifiedValues && element.name === reference.name && element.value !== element.defaultValue)
) {
this.#updateProperty(element, "value", ref.value);
this.#updateProperty(element, "value", reference.value);

const text = element.firstElementChild;
if (text) this.#updateProperty(text, "textContent", ref.value);
if (text) this.#updateProperty(text, "textContent", reference.value);
}
}

// Iterates over the child nodes of the reference element, morphing the main element’s child nodes to match.
#morphChildNodes(element: Element, ref: ReadonlyNode<Element>): void {
#morphChildNodes(element: Element, reference: ReadonlyNode<Element>): void {
const childNodes = element.childNodes;
const refChildNodes = ref.childNodes;
const refChildNodes = reference.childNodes;

for (let i = 0; i < refChildNodes.length; i++) {
const child = childNodes[i] as ChildNode | null;
const refChild = refChildNodes[i] as ReadonlyNode<ChildNode> | null;

if (child && refChild) {
if (isElement(child) && isElement(refChild) && child.localName === refChild.localName) {
if (isHead(child)) this.#morphHead(child, refChild as ReadonlyNode<HTMLHeadElement>);
else this.#morphChildElement(child, refChild, element);
} else this.#morphNode(child, refChild); // TODO: performance optimization here
if (isHead(child)) {
this.#morphHead(child, refChild as ReadonlyNode<HTMLHeadElement>);
} else this.#morphChildElement(child, refChild, element);
} else this.#morphOtherNode(child, refChild);
} else if (refChild) {
this.#appendChild(element, refChild.cloneNode(true));
} else if (child) {
Expand All @@ -256,10 +271,10 @@ class Morph {
}
}

#morphChildElement(child: Element, ref: ReadonlyNode<Element>, parent: Element): void {
if (!(this.#options.beforeNodeMorphed?.(child, ref as ChildNode) ?? true)) return;
#morphChildElement(child: Element, reference: ReadonlyNode<Element>, parent: Element): void {
if (!(this.#options.beforeNodeMorphed?.(child, reference as ChildNode) ?? true)) return;

const refIdSet = this.#idMap.get(ref);
const refIdSet = this.#idMap.get(reference);

// Generate the array in advance of the loop
const refSetArray = refIdSet ? [...refIdSet] : [];
Expand All @@ -272,20 +287,20 @@ class Morph {
if (isElement(currentNode)) {
const id = currentNode.id;

if (!nextMatchByTagName && currentNode.localName === ref.localName) {
if (!nextMatchByTagName && currentNode.localName === reference.localName) {
nextMatchByTagName = currentNode;
}

if (id !== "") {
if (id === ref.id) {
if (id === reference.id) {
this.#insertBefore(parent, currentNode, child);
return this.#morphNode(currentNode, ref);
return this.#morphNode(currentNode, reference);
} else {
const currentIdSet = this.#idMap.get(currentNode);

if (currentIdSet && refSetArray.some((it) => currentIdSet.has(it))) {
this.#insertBefore(parent, currentNode, child);
return this.#morphNode(currentNode, ref);
return this.#morphNode(currentNode, reference);
}
}
}
Expand All @@ -296,20 +311,21 @@ class Morph {

if (nextMatchByTagName) {
this.#insertBefore(parent, nextMatchByTagName, child);
this.#morphNode(nextMatchByTagName, ref);
this.#morphNode(nextMatchByTagName, reference);
} else {
const newNode = ref.cloneNode(true);
const newNode = reference.cloneNode(true);
if (this.#options.beforeNodeAdded?.(newNode) ?? true) {
this.#insertBefore(parent, newNode, child);
this.#options.afterNodeAdded?.(newNode);
}
}

this.#options.afterNodeMorphed?.(child, ref as ChildNode);
this.#options.afterNodeMorphed?.(child, reference as ChildNode);
}

#updateProperty<N extends Node, P extends keyof N>(node: N, propertyName: P, newValue: N[P]): void {
const previousValue = node[propertyName];

if (previousValue !== newValue && (this.#options.beforePropertyUpdated?.(node, propertyName, newValue) ?? true)) {
node[propertyName] = newValue;
this.#options.afterPropertyUpdated?.(node, propertyName, previousValue);
Expand Down Expand Up @@ -341,9 +357,7 @@ class Morph {

if (previousNode === insertionPoint) return;
previousNode = node.previousSibling;
} else {
break;
}
} else break;
}
}
}
Expand Down

0 comments on commit e83be73

Please sign in to comment.