// This is pretty hot code so it's important that we pay good consideration to // writing performant code even if sometimes that means writing less clean code. // I'm not exactly a perf witch though, but here are some resources I've collected // along the way: // // - https://romgrk.com/posts/optimizing-javascript // - https://www.zhenghao.io/posts/object-vs-map // // Morph turns a Lustre VDOM node into real DOM nodes. Instead of doing a VDOM // diff that produces a patch, we morph the VDOM into the real DOM directly. export function morph(prev, next, dispatch, isComponent = false) { // This function eventually returns the morphed root node. Because the morphing // process might involve _removing_ the root in some cases, we can't simply return // `prev` and hope for the best. // // We've also unfolded the recursive implementation into a stack-based iterative // one so we cant just rely on good ol' recursion to return the root node. Instead // we track it here and make sure to only set it once. let out; // A stack of nodes to still left to morph. This will shrink and grow over the // course of the function. *Either* `prev` or `next` can be missing, but never // both. The `parent` is *always* present. let stack = [{ prev, next, parent: prev.parentNode }]; while (stack.length) { let { prev, next, parent } = stack.pop(); // If we have the `subtree` property then we're looking at a `Map` vnode that // is lazily evaluated. We'll force it here and then proceed with the morphing. if (next.subtree !== undefined) next = next.subtree(); // Text nodes: if (next.content !== undefined) { if (!prev) { const created = document.createTextNode(next.content); parent.appendChild(created); out ??= created; } else if (prev.nodeType === Node.TEXT_NODE) { if (prev.textContent !== next.content) prev.textContent = next.content; out ??= prev; } else { const created = document.createTextNode(next.content); parent.replaceChild(created, prev); out ??= created; } } // Element nodes: else if (next.tag !== undefined) { const created = createElementNode({ prev, next, dispatch, stack, isComponent, }); if (!prev) { parent.appendChild(created); } // We can morph the new node into the previous one if they are compatible. // In those cases we wouldn't want to waste time doing a `replaceChild` so // we're checking explicitly if the new node is different from the previous // one. else if (prev !== created) { parent.replaceChild(created, prev); } out ??= created; } // If this happens, then the top level Element is a Fragment `prev` should be // the first element of the given fragment. Functionally, a fragment as the // first child means that document -> body will be the parent of the first level // of children else if (next.elements !== undefined) { iterateElement(next, (fragmentElement) => { stack.unshift({ prev, next: fragmentElement, parent }); prev = prev?.nextSibling; }); } else if (next.subtree !== undefined) { stack.push({ prev, next, parent }); } } return out; } export function patch(root, diff, dispatch) { const rootParent = root.parentNode; // A diff is a tuple of three arrays: created, removed, updated. Each of these // arrays contains tuples of slightly differing shape. You'll have to go peek // at `src/lustre/internals/patch.gleam` to work out the exact shape. // A `created` diff is a tuple of `[key, element]` where the `key` is a path // to the element in the VDOM tree and the `element` is the VDOM node itself. // Nodes don't have any optimised encoding so they can be passed to `morph` // without any processing. // // We get a created diff if the element is brand new *or* if it changed the tag // of the element. for (const created of diff[0]) { const key = created[0].split("-"); const next = created[1]; const prev = getDeepChild(rootParent, key); let result; // If there was a previous node then we can morph the new node into it. if (prev !== null && prev !== rootParent) { result = morph(prev, next, dispatch); } // Otherwise, we create a temporary node to hold the new node's place in the // tree. This can happen because we might get a patch that tells us some node // was created at a path that doesn't exist yet. else { const parent = getDeepChild(rootParent, key.slice(0, -1)); const temp = document.createTextNode(""); parent.appendChild(temp); result = morph(temp, next, dispatch); } // Patching the root node means we might end up replacing it entirely so we // need to reassign the root node if the key is "0". if (key === "0") { root = result; } } // A `removed` diff is just a one-element tuple (for consistency) of the key of // the removed element. for (const removed of diff[1]) { const key = removed[0].split("-"); const deletedNode = getDeepChild(rootParent, key); deletedNode.remove(); } // An `updated` diff is all about *attributes*. It's a tuple of `[key, patches]` // where patches is another list of diffs. for (const updated of diff[2]) { const key = updated[0].split("-"); const patches = updated[1]; const prev = getDeepChild(rootParent, key); const handlersForEl = registeredHandlers.get(prev); for (const created of patches[0]) { const name = created[0]; const value = created[1]; if (name.startsWith("data-lustre-on-")) { const eventName = name.slice(15); const callback = dispatch(lustreServerEventHandler); if (!handlersForEl.has(eventName)) { el.addEventListener(eventName, lustreGenericEventHandler); } handlersForEl.set(eventName, callback); el.setAttribute(name, value); } else { prev.setAttribute(name, value); prev[name] = value; } } for (const removed of patches[1]) { if (removed[0].startsWith("data-lustre-on-")) { const eventName = removed[0].slice(15); prev.removeEventListener(eventName, lustreGenericEventHandler); handlersForEl.delete(eventName); } else { prev.removeAttribute(removed[0]); } } } return root; } // CREATING ELEMENTS ----------------------------------------------------------- // // @todo do we need to special-case ``, `