/** * Hoover — render.js * * Render engine: render(container, fn), container-level diffing, * batched re-render loop integration with reactivity.js. */ import { requestUpdate, setCommitFn } from './reactivity.js?v=9'; import { _vnodeDom, createDom, getDom, patchNode, sweepDom, setMountFn, setUnmountFn, } from './vdom.js?v=9'; import { mountComponent, unmountComponent } from './component.js?v=9'; /** Container → previous root vnodes */ export const _renderSlots = new Map(); /** Container → render function */ export const _renderFns = new Map(); /** Component key → last normalized #comp output (for _vnodeDom preservation) */ export const _compExpandedCache = new Map(); /** Component key → renderer function (survives normalization that expands #comp) */ const _compRegistry = new Map(); /** * Set up lifecycle callback hooks from vdom.js. * Called once during render initialization. */ setMountFn((el) => { // Reserved for future DOM-level mount hooks }); setUnmountFn((el) => { // Called during sweepDom for cleanup }); /** * Commit callback: re-renders all registered containers in batch. * Set as the callback for reactivity.js's requestUpdate(). */ function commitAll() { for (const container of _renderFns.keys()) { commit(container); } } setCommitFn(commitAll); /** * Mount a render function onto a DOM container. * - First call: create DOM from scratch, append to container * - Subsequent calls: diff against previous VNodes, patch in place */ export function render(container, fn) { _renderFns.set(container, fn); commit(container); } /** * Evaluate render function, diff vs previous, commit to _renderSlots. */ function commit(container) { const fn = _renderFns.get(container); if (!fn) return; let result = fn(); if (typeof result === 'function') result = result(); const prev = _renderSlots.get(container); // Normalize: expand #comp vnodes and track lifecycle const vnodes = normalizeVNodesWithLifecycle(result, prev); if (!prev) { for (const v of vnodes) { const d = createDom(v); _vnodeDom.set(v, d); container.appendChild(d); } } else { diffContainer(container, prev, vnodes); } _renderSlots.set(container, vnodes); } /** * Normalize render output: filter nulls, expand #comp vnodes, * and manage component lifecycle based on key changes. */ function normalizeVNodesWithLifecycle(result, prevVnodes) { const oldEntries = [..._compRegistry.entries()].map(([key, renderer]) => ({ key, renderer })); const oldKeyMap = new Map(oldEntries.map(e => [e.key, e])); const newEntries = []; const normalized = normalizeRecursive(result, oldKeyMap, newEntries); for (const entry of oldEntries) { if (!newEntries.some(e => e.key === entry.key)) { unmountComponent(entry.key, entry.renderer); } } for (const entry of newEntries) { if (!oldKeyMap.has(entry.key)) { mountComponent(entry.key, entry.renderer); } } // Sync registry with current render (prevVnodes are normalized and lack #comp tags, // so collectCompEntries always returns [] after the first render) const newKeySet = new Set(newEntries.map(e => e.key)); for (const [key] of _compRegistry) { if (!newKeySet.has(key)) _compRegistry.delete(key); } for (const entry of newEntries) { _compRegistry.set(entry.key, entry.renderer); } return normalized; } /** * Recursively normalize a value to a flat VNode array, expanding * #comp vnodes into their rendered content while tracking lifecycle. * * When prevCh is provided, preserves _vnodeDom entries so that diff * can locate existing DOM after normalization creates new vnode objects. */ function normalizeRecursive(result, oldKeyMap, newEntries, prevCh) { if (result == null) return []; if (Array.isArray(result)) { const flat = []; let idx = 0; for (const item of result) { flat.push(...normalizeRecursive(item, oldKeyMap, newEntries, prevCh && prevCh[idx])); idx++; } return flat; } const vnode = result; if (typeof vnode !== 'object') return [{ tag: '#text', text: String(vnode) }]; if (vnode.tag === '#text') return [vnode]; if (vnode.tag === '#comp') { const renderer = vnode.props?.component; const key = vnode.props?.key; if (key !== undefined) { const existing = newEntries.find(e => e.key === key); if (!existing) newEntries.push({ key, renderer }); } if (renderer && typeof renderer === 'function') { const content = renderer(); const prevExpanded = key !== undefined ? _compExpandedCache.get(key) : null; const result = normalizeRecursive(content, oldKeyMap, newEntries, prevExpanded); if (key !== undefined) _compExpandedCache.set(key, result); return result; } return normalizeRecursive([...(vnode.ch || [])], oldKeyMap, newEntries, prevCh); } const rawChildren = vnode.ch || []; const prevChildren = prevCh && prevCh.ch ? prevCh.ch : null; const children = []; for (let i = 0; i < rawChildren.length; i++) { const normalized = normalizeRecursive(rawChildren[i], oldKeyMap, newEntries, prevChildren && prevChildren[i]); children.push(...normalized); } const newVNode = { tag: vnode.tag, props: vnode.props, ch: children }; // Preserve _vnodeDom entry: if the old vnode at this position had a // DOM association, transfer it to the new normalized vnode so diff // can locate existing DOM without creating duplicates. if (prevCh && _vnodeDom.has(prevCh)) { _vnodeDom.set(newVNode, _vnodeDom.get(prevCh)); } return [newVNode]; } /** Collect all #comp entries {key, renderer} from a vnode tree. */ function collectCompEntries(vnodes, entries) { for (const v of vnodes || []) { if (!v) continue; if (v.tag === '#comp') { const key = v.props?.key; const renderer = v.props?.component; if (key !== undefined) entries.push({ key, renderer }); } if (v.ch) collectCompEntries(v.ch, entries); } return entries; } /** * Diff two VNode arrays inside a container, patching in place. * * Fix: anchor tracking ensures correct DOM insertion order. * Fix: _vnodeDom updated after every patch. */ function diffContainer(container, prev, vnodes) { const maxLen = Math.max(vnodes.length, prev.length); let lastDom = null; for (let i = 0; i < maxLen; i++) { const oldV = prev[i], newV = vnodes[i]; if (!newV && oldV) { const d = getDom(oldV); if (d?.parentNode) { if (d.nodeType === Node.ELEMENT_NODE) sweepDom(d); d.parentNode.removeChild(d); lastDom = i > 0 ? getDom(prev[i - 1]) : null; } continue; } if (newV && !oldV) { const d = createDom(newV); _vnodeDom.set(newV, d); container.insertBefore(d, lastDom ? lastDom.nextSibling : null); lastDom = d; continue; } const oldDom = getDom(oldV); if (oldDom && oldV.tag === newV.tag) { patchNode(oldDom.nodeType === 1 ? oldDom.parentNode : container, oldV, newV, null); lastDom = getDom(newV); } else if (oldDom && !oldDom.parentNode) { // oldDom exists in _vnodeDom but detached from the tree if (oldDom.nodeType === Node.ELEMENT_NODE) sweepDom(oldDom); const nd = createDom(newV); _vnodeDom.set(newV, nd); container.insertBefore(nd, lastDom ? lastDom.nextSibling : null); lastDom = nd; } else if (oldDom && oldV.tag !== newV.tag) { // tag mismatch — replace old DOM with new if (oldDom.nodeType === Node.ELEMENT_NODE) sweepDom(oldDom); const nd = createDom(newV); _vnodeDom.set(newV, nd); oldDom.parentNode.replaceChild(nd, oldDom); lastDom = nd; } else { // oldDom is null — create and insert new DOM const nd = createDom(newV); _vnodeDom.set(newV, nd); container.insertBefore(nd, lastDom ? lastDom.nextSibling : null); lastDom = nd; } } }