import { FiberTag, EffectTag } from './fiber.ts'; import { cleanUpEffects } from './hooks'; import { assert } from './utils.ts'; const { HostFiber, HostTextFiber, } = FiberTag; const { Placement, } = EffectTag; export function commitTree(fiber) { fiber.current = true; if (fiber.alternate) fiber.alternate.current = false; if (fiber.child) commit(fiber.child); } function commit(fiber) { commitWork(fiber); if (fiber.child) commit(fiber.child); if (fiber.sibling) commit(fiber.sibling); } function commitWork(fiber) { fiber.current = true; if (fiber.alternate) fiber.alternate.current = false; if (fiber.tag == HostTextFiber) { if (fiber.effect === 'placement') commitTextPlacement(fiber); else commitTextUpdate(fiber); } else if (fiber.tag == HostFiber) { if (fiber.effect === 'placement') commitElementPlacement(fiber); else commitElementUpdate(fiber); } if (fiber.deletedChildren) { for (const deleteChild of fiber.deletedChildren) { commitDeletion(deleteChild); } } } function commitDeletion(fiber) { const domParent = findDomParent(fiber); removeAllChildren(domParent, fiber); } function removeAllChildren(domParent, forFiber) { let fiber = forFiber; while (true) { if (fiber.tag === HostFiber || fiber.tag === HostTextFiber) { // There is no need to remove DOM nodes for ancestors of this node // because the DOM will remove them for us, but we do still need to // traverse the subtree and unmount its components, // and that happens in `unmountFiberTree()` unmountFiberTree(fiber); // A host fiber which is being deleted must have already been placed, // so it must have a DOM node assert(fiber.dom); domParent.removeChild(fiber.dom); } else { // Unlike the above Host case, it is okay to unmount only this fiber // because we are going to traverse its children within this loop unmountFiber(fiber); // This is a non-host fiber so it has no DOM node, but its children might, // so we must traverse them looking for host fibers if (fiber.child) { fiber = fiber.child; continue; } } // We have reached the original `forFiber` (or it had no children), // so we're done if (fiber === forFiber) return; // At this point we must be within a descendant of `forFiber` // // If this (child) fiber has no sibling we must go up the tree until we find // a parent with a sibling // (this "parent" will still be a descendant of `forFiber`) while (!fiber.sibling) { // If we hit the root, we must be done if (!fiber.parent) return; // If we hit the original `forFiber`, we must be done if (fiber.parent === forFiber) return; fiber = fiber.parent; } // Continue with the next sibling (which must be a descendant of `forFiber`) fiber = fiber.sibling; } } function unmountFiberTree(rootFiber) { let fiber = rootFiber; while (true) { unmountFiber(fiber); if (fiber.child) { fiber = fiber.child; continue; } if (fiber === rootFiber) return; while (!fiber.sibling) { if (!fiber.parent) return; if (fiber.parent === rootFiber) return; fiber = fiber.parent; } fiber = fiber.sibling; } } function unmountFiber(fiber) { cleanUpEffects(fiber); fiber.current = false; if (fiber.alternate) assert(fiber.alternate.current === false); } function commitTextPlacement(fiber) { const domParent = findDomParent(fiber); const domSibling = findNextExistingDomSibling(fiber); const domNode = document.createTextNode(fiber.props.nodeValue); fiber.dom = domNode; if (domSibling) domParent.insertBefore(domNode, domSibling); else domParent.appendChild(domNode, domSibling); } function commitTextUpdate(fiber) { const domNode = fiber.dom; assert(domNode.nodeType === Node.TEXT_NODE); // TODO: diff during reconciliation? const oldValue = fiber.alternate.props.nodeValue; const newValue = fiber.props.nodeValue; if (newValue !== oldValue) domNode.nodeValue = newValue; } function commitElementPlacement(fiber) { const domParent = findDomParent(fiber); const domSibling = findNextExistingDomSibling(fiber); const domNode = document.createElement(fiber.type); fiber.dom = domNode; if (fiber.props.ref) fiber.props.ref.current = domNode; if (domSibling) domParent.insertBefore(domNode, domSibling); else domParent.appendChild(domNode); diffProps(fiber); } function commitElementUpdate(fiber) { diffProps(fiber); } function diffProps(fiber) { const domNode = fiber.dom; assert(domNode); // TODO: diff during reconciliation? const oldProps = fiber.alternate ? fiber.alternate.props : {}; const newProps = fiber.props; // Build lookup for old props const oldLookup = new Map(); for (const [key, oldVal] of Object.entries(oldProps)) { if (key === 'children') continue; oldLookup.set(key, oldVal); } // Diff new props, removing keys from oldLookup if present for (const [key, newVal] of Object.entries(newProps)) { if (key === 'children') continue; const oldVal = oldLookup.get(key); oldLookup.delete(key); if (isEventPropKey(key)) { const eventName = eventForPropKey(key); if (oldVal !== undefined) domNode.removeEventListener(eventName, oldVal); domNode.addEventListener(eventName, newVal); } else { if (newVal !== oldVal) { if (fiber.type === 'input' && key === 'value') { if (newVal === false || newVal === null || newVal === undefined) domNode.value = null; else domNode.value = newVal; } else if (fiber.type === 'input' && key === 'checked') { if (newVal === false || newVal === null || newVal === undefined) domNode.checked = false; else domNode.checked = true; } else{ if (newVal === false || newVal === null || newVal === undefined) domNode.removeAttribute(key); else if (newVal === true) domNode.setAttribute(key, ''); else domNode.setAttribute(key, newVal); } } } } // Remove remaining old props that were not found in newProps oldLookup.forEach((oldVal, key) => { if (isEventPropKey(key)) domNode.removeEventListener(eventForPropKey(key), oldVal); else domNode.removeAttribute(key); }); } function isEventPropKey(key) { return key.startsWith('on'); } function eventForPropKey(key) { return key.substring(2).toLowerCase(); } function findDomParent(forFiber) { let fiber = forFiber.parent; while (fiber) { if (fiber.dom) return fiber.dom; fiber = fiber.parent; } throw new Error(`Could not find parent for fiber ${forFiber}`); } // Find the next DOM sibling for `forFiber` which already exists (from the last render) // // Naively this could be done by simply traversing the tree siblings of the fiber (`fiber.sibling`), // however non-host (function component) fibers do not have DOM nodes, so the DOM sibling of a // given fiber may be above *or* below it in the fiber tree function findNextExistingDomSibling(forFiber) { let fiber = forFiber; siblingIter: while (true) { // If the current node has no siblings, traverse up the tree // looking for DOM siblings while(!fiber.sibling) { // If we hit the root (no parent) then there are no more nodes to check if (!fiber.parent) return null; // If we hit a Host fiber, that must the the DOM parent of `forFiber`, // so there are no more siblings to check // (we do not check for a HostTextFiber because text nodes cannot be parents) if (fiber.parent.tag == HostFiber) return null; assert(fiber.parent.tag !== HostTextFiber); fiber = fiber.parent; } fiber = fiber.sibling; // If `fiber` is a non-host fiber, traverse down the tree to find DOM nodes within it while (fiber.tag !== HostFiber && fiber.tag !== HostTextFiber) { // If this (non-host) sibling is being placed during this render, it cannot have DOM children, // so we continue to the next sibling if (fiber.effect === Placement) continue siblingIter; // If this (non-host) sibling has no children we continue to the next sibling if (!fiber.child) continue siblingIter; fiber = fiber.child; } // At this point `fiber` must be a Host sibling // // If `fiber` is being placed during this render it cannot have a DOM node, // so we continue to the next sibling if (fiber.effect === Placement) continue; assert(fiber.dom); return fiber.dom; } }