import { WDom, TagFunctionResolver, RenderType, Props } from '@/types'; import { checkCustemComponentFunction, getKey } from '@/utils/predicator'; import { getParent, makeParentGetter } from '@/utils'; import { typeDeleteUnused } from '@/render'; import { cloneWDom, copyResolver } from '@/wDom'; import { checkEmptyElement, checkSameWDomWithOriginal, getWDomType, checkExistyKey, checkExisty, checkKeyedChildren, } from '@/utils/predicator'; import { runUnmountQueueFromWDom } from '@/hook/internal/unmount'; import { keys, isEnumerableProp } from '@/utils'; /** * The starting point of the diffing process between the original virtual DOM and the new virtual DOM for re-rendering. */ export const makeNewWDomTree = ( newWDom: WDom | TagFunctionResolver, originalWDom?: WDom ) => { // Already drawn or taken by a diff (it has nr, 'N' once drawn): the node // belongs to another place, so this place draws a copy. The same node in the same place is still left // alone as unchanged. A component made without a diff and not drawn yet // (in a new component's output, or a hoisted ) is drawn as it is: // a copy would be a second instance, leaving the one already set up behind. if (newWDom !== originalWDom && (newWDom as WDom).nr) { newWDom = cloneWDom(newWDom as WDom); } return remakeNewWDom( newWDom, !!originalWDom && checkSameWDomWithOriginal[getWDomType(newWDom)](newWDom, originalWDom), originalWDom ); }; /** * Create a new virtual DOM after comparing with the previous virtual DOM */ const remakeNewWDom = ( newWDom: WDom | TagFunctionResolver, isSameType: boolean, originalWDom?: WDom ) => { const remakeWDom = generalize(newWDom, isSameType, originalWDom); const needRerender = addReRenderTypeProperty( remakeWDom, isSameType, originalWDom ); const isNoting = needRerender === 'N'; if (!isNoting) { remakeWDom.children = remakeChildrenForDiff( remakeWDom, isSameType, originalWDom ); } // NOTE: short-key metadata (nr = needRerender) keeps bundle size down remakeWDom.nr = needRerender; inheritPropForRender(remakeWDom, originalWDom, needRerender); // A retired node keeps its children: it may be a hoisted constant the // caller draws again later, which is then copied from them. A component's // node is copied from its description instead (cloneWDom), so it lets go // of its previous output: otherwise every row a list redraws keeps its old // subtree reachable, for the GC to copy, until the whole redraw is done. if (!isNoting && originalWDom) { originalWDom.il = true; if (originalWDom.ctor) { originalWDom.children = undefined; } } return remakeWDom; }; /** * Handle what the new virtual DOM should inherit or reconcile from the original virtual DOM. */ const inheritPropForRender = ( remakeWDom: WDom, originalWDom?: WDom, needRerender?: RenderType ) => { if (needRerender !== 'A' && originalWDom) { remakeWDom.el = originalWDom.el; } if (needRerender === 'D' || needRerender === 'R' || needRerender === 'S') { if (originalWDom) { runUnmountQueueFromWDom(originalWDom); } remakeWDom.oc = originalWDom && originalWDom.children; } remakeWDom.op = originalWDom && originalWDom.props; }; /** * Determine the render operation type (Add/Delete/Replace/Update/etc) * based on comparison between new and original WDom */ const addReRenderTypeProperty = ( newWDom: WDom, isSameType: boolean, originalWDom?: WDom ): RenderType | undefined => { if (checkEmptyElement(newWDom)) return 'D'; if ( newWDom === originalWDom || (isSameType && newWDom.type === 't' && newWDom.text === (originalWDom as WDom).text) ) { return 'N'; } if (!(originalWDom && originalWDom.type)) return 'A'; const parent = getParent(originalWDom); const isKeyChecked = !newWDom.isRoot && parent && parent.type === 'l' && checkExistyKey(newWDom); return isSameType ? (isKeyChecked ? 'T' : 'U') : isKeyChecked ? 'S' : 'R'; }; /** * Handling virtual DOM attribute updates. */ const syncResolverProps = (props: Props, infoProps: Props) => { if (props && infoProps !== props) { // Only the keys that are gone: deleting and re-adding every key on every // render changes the object's shape each time. infoProps = infoProps || {}; keys(props).forEach( key => isEnumerableProp(infoProps, key) || delete props[key] ); keys(infoProps).forEach(key => (props[key] = infoProps[key])); } }; /** * Keep the existing component node while injecting the props/children from the freshly computed TagFunctionResolver. * originalWDom is the fully evaluated node from the previous render, whereas infoVdom is the intermediate resolver, * so updating compProps/compChild in place lets reRender() evaluate with the latest slots and props. * It is crucial that props and children references remain stable as nodes update—many ancestors/closures share these arrays, * so mutate the existing arrays instead of replacing their references. */ const runUpdate = (vDom: WDom, infoVdom: TagFunctionResolver) => { const { compProps: props, compChild: children } = vDom; const { props: infoProps, children: infoChidren } = infoVdom; if (props) { syncResolverProps(props, infoProps); } /** * In runUpdate, the original DOM (vDom) is already fully evaluated and completed, whereas infoVdom represents an intermediate state. Therefore, vdom.compChild and infoVdom.children are indeed the correct counterparts to compare. * Keep shared child array references intact (<= same slot array instance) * When the reference changes, sync the stored compChild contents so reRender sees fresh nodes * Only update children if they're different references * If it's the same Fragment reference, the diff process will handle updating its children */ if (children && infoChidren && children !== infoChidren) { children.splice(0, children.length, ...infoChidren); } return (vDom.reRender && vDom.reRender()) as WDom; }; /** * Replace the compared virtual DOM with the new virtual DOM or simply update it. */ const generalize = ( newWDom: WDom | TagFunctionResolver, isSameType: boolean, originalWDom?: WDom ): WDom => { return checkCustemComponentFunction(newWDom) ? isSameType && originalWDom ? runUpdate(originalWDom, newWDom) : // Resolved before (children a component draws again somewhere else): // the first instance owns its props, which are also its key, so the // new instance is made from a copy. (newWDom.rd ? copyResolver(newWDom) : ((newWDom.rd = true), newWDom) ).resolve() : newWDom; }; /** * 자식 가상돔들도 전부 재귀처리하며 똑같은 처리를 해준다. */ const remakeChildrenForDiff = ( newWDom: WDom, isSameType: boolean, originalWDom?: WDom ) => isSameType && originalWDom ? remakeChildrenForUpdate(newWDom, originalWDom) : remakeChildrenForAdd(newWDom); /** * Recursive handling for the creation of a new virtual DOM. Kept apart from * the update below: one function for both made creating 1,000 rows 6% slower. */ const remakeChildrenForAdd = (newWDom: WDom) => { const getParent = makeParentGetter(newWDom); return (newWDom.children || []).map((item: WDom) => { const child = makeNewWDomTree(item); child.getParent = getParent; return child; }); }; /** * Recursive handling for updates, not additions. * Uses key-based diffing for loops, index-based for others */ const remakeChildrenForUpdate = (newWDom: WDom, originalWDom: WDom) => { if (newWDom.type === 'l' && checkKeyedChildren(newWDom)) { return diffLoopChildren(newWDom, originalWDom); } const origChildren = originalWDom.children || []; const getParent = makeParentGetter(newWDom); return (newWDom.children || []).map((item: WDom, index: number) => { const child = makeNewWDomTree(item, origChildren[index]); child.getParent = getParent; return child; }); }; /** * Recursive handling of loop-type virtual DOM elements. * Matches children against the originals by key via a Map in O(n) and * records each match's original index (oi) for the render phase to * minimize moves via LIS. The originals left unmatched are deleted. */ const diffLoopChildren = (newWDom: WDom, originalWDom: WDom) => { const origChildren = originalWDom.children || []; const keyMap = new Map(); origChildren.forEach((item, index) => { const key = getKey(item); if (checkExisty(key) && !keyMap.has(key)) { keyMap.set(key, index); } }); const used: boolean[] = []; const getParent = makeParentGetter(newWDom); const remaked = (newWDom.children || []).map((item, index) => { const key = getKey(item); // Unkeyed items among keyed ones (a hidden row's null, a nested array) // pair with the unkeyed original at the same index, as in React. const origIndex = checkExisty(key) ? keyMap.get(key) : index < origChildren.length && !checkExistyKey(origChildren[index]) ? index : undefined; let original: WDom | undefined; if (origIndex !== undefined) { keyMap.delete(key); used[origIndex] = true; original = origChildren[origIndex]; } const child = makeNewWDomTree(item, original); if (original) { child.oi = origIndex; } child.getParent = getParent; return child; }); // Originals left unmatched, including ones whose key was already taken // (no key, or a duplicate) and so never made it into keyMap. typeDeleteUnused( origChildren.filter((_item, index) => !used[index]), originalWDom ); return remaked; };