diff --git a/src/fiber/beginWork.ts b/src/fiber/beginWork.ts new file mode 100644 index 0000000..3fd5c1c --- /dev/null +++ b/src/fiber/beginWork.ts @@ -0,0 +1,501 @@ +/* **************** */ +/* Begin Work - Render Phase Entry */ +/* **************** */ + +/** + * Implements the "begin" phase of fiber reconciliation. + * This is where we decide what work needs to be done for each fiber. + */ + +import type { AnyMiniReactElement, FunctionalComponent } from "../core/types"; +import { mountChildFibers, reconcileChildFibers } from "./childReconciler"; +import { renderWithHooks } from "./fiberHooks"; +import type { Fiber, FiberRoot, Lanes } from "./types"; +import { NoFlags, NoLanes, WorkTag } from "./types"; +import { + getDidReceiveUpdate, + markWorkInProgressReceivedUpdate, +} from "./workInProgress"; + +// ============================================ +// Begin Work Entry Point +// ============================================ + +/** + * Performs the "begin" phase of work on a fiber. + * Returns the next fiber to work on (child) or null if done with this branch. + */ +export function beginWork( + current: Fiber | null, + workInProgress: Fiber, + renderLanes: Lanes, +): Fiber | null { + // Check for bailout opportunity + if (current !== null) { + const oldProps = current.memoizedProps; + const newProps = workInProgress.pendingProps; + + if (oldProps !== newProps) { + markWorkInProgressReceivedUpdate(); + } else if (!hasScheduledWork(current, renderLanes)) { + // No pending work - can we bail out? + return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes); + } + } + + // Reset the lanes since we're about to process this fiber + workInProgress.lanes = NoLanes; + + // Dispatch to tag-specific handler + switch (workInProgress.tag) { + case WorkTag.FunctionComponent: + return updateFunctionComponent( + current, + workInProgress, + workInProgress.type as FunctionalComponent, + workInProgress.pendingProps, + renderLanes, + ); + + case WorkTag.HostRoot: + return updateHostRoot(current, workInProgress, renderLanes); + + case WorkTag.HostComponent: + return updateHostComponent(current, workInProgress, renderLanes); + + case WorkTag.HostText: + return updateHostText(current, workInProgress); + + case WorkTag.Fragment: + return updateFragment(current, workInProgress, renderLanes); + + case WorkTag.HostPortal: + return updatePortal(current, workInProgress, renderLanes); + + case WorkTag.ContextProvider: + return updateContextProvider(current, workInProgress, renderLanes); + + case WorkTag.ContextConsumer: + return updateContextConsumer(current, workInProgress, renderLanes); + + case WorkTag.MemoComponent: + return updateMemoComponent( + current, + workInProgress, + workInProgress.type as FunctionalComponent, + workInProgress.pendingProps, + renderLanes, + ); + + default: + throw new Error(`Unknown fiber tag: ${workInProgress.tag}`); + } +} + +// ============================================ +// Tag-Specific Update Functions +// ============================================ + +/** + * Updates a function component. + */ +function updateFunctionComponent( + current: Fiber | null, + workInProgress: Fiber, + Component: FunctionalComponent, + nextProps: Record, + renderLanes: Lanes, +): Fiber | null { + // Render the component with hooks support + const nextChildren = renderWithHooks( + current, + workInProgress, + Component, + nextProps, + renderLanes, + ); + + if (current !== null && !getDidReceiveUpdate()) { + // Bailout - no updates + return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes); + } + + // Reconcile children + reconcileChildren(current, workInProgress, nextChildren, renderLanes); + + return workInProgress.child; +} + +/** + * Updates the host root fiber. + */ +function updateHostRoot( + current: Fiber | null, + workInProgress: Fiber, + renderLanes: Lanes, +): Fiber | null { + // Get the pending element from the root + const root = workInProgress.stateNode as FiberRoot; + const nextChildren = root.pendingChildren; + + reconcileChildren(current, workInProgress, nextChildren, renderLanes); + + return workInProgress.child; +} + +/** + * Updates a host component (DOM element). + */ +function updateHostComponent( + current: Fiber | null, + workInProgress: Fiber, + renderLanes: Lanes, +): Fiber | null { + // Get children from props + const nextProps = workInProgress.pendingProps; + let nextChildren = nextProps["children"] as AnyMiniReactElement | undefined; + + // Handle text-only children + if (typeof nextChildren === "string" || typeof nextChildren === "number") { + // Text content is handled directly, no child fibers + nextChildren = undefined; + } + + reconcileChildren(current, workInProgress, nextChildren, renderLanes); + + return workInProgress.child; +} + +/** + * Updates a host text node. + */ +function updateHostText( + _current: Fiber | null, + _workInProgress: Fiber, +): Fiber | null { + // Text nodes have no children + return null; +} + +/** + * Updates a fragment. + */ +function updateFragment( + current: Fiber | null, + workInProgress: Fiber, + renderLanes: Lanes, +): Fiber | null { + const nextChildren = workInProgress.pendingProps[ + "children" + ] as AnyMiniReactElement; + reconcileChildren(current, workInProgress, nextChildren, renderLanes); + return workInProgress.child; +} + +/** + * Updates a portal. + */ +function updatePortal( + current: Fiber | null, + workInProgress: Fiber, + renderLanes: Lanes, +): Fiber | null { + const nextChildren = workInProgress.pendingProps[ + "children" + ] as AnyMiniReactElement; + reconcileChildren(current, workInProgress, nextChildren, renderLanes); + return workInProgress.child; +} + +/** + * Updates a context provider. + */ +function updateContextProvider( + current: Fiber | null, + workInProgress: Fiber, + renderLanes: Lanes, +): Fiber | null { + // Push context value would happen here + const newProps = workInProgress.pendingProps; + const children = newProps["children"] as AnyMiniReactElement; + + // Store the value on the fiber + workInProgress.memoizedProps = newProps; + + reconcileChildren(current, workInProgress, children, renderLanes); + return workInProgress.child; +} + +/** + * Updates a context consumer. + */ +function updateContextConsumer( + current: Fiber | null, + workInProgress: Fiber, + renderLanes: Lanes, +): Fiber | null { + // Read context value would happen here + const newProps = workInProgress.pendingProps; + const render = newProps["children"] as ( + value: unknown, + ) => AnyMiniReactElement; + + // Get context value - placeholder + const contextValue = undefined; + const newChildren = render(contextValue); + + reconcileChildren(current, workInProgress, newChildren, renderLanes); + return workInProgress.child; +} + +/** + * Updates a memo component. + */ +function updateMemoComponent( + current: Fiber | null, + workInProgress: Fiber, + Component: FunctionalComponent, + nextProps: Record, + renderLanes: Lanes, +): Fiber | null { + if (current !== null) { + const prevProps = current.memoizedProps; + + if (prevProps !== null) { + // Check for custom comparison function from __memo + const memoInfo = ( + workInProgress.type as unknown as Record + )["__memo"] as + | { + Component: FunctionalComponent; + areEqual: ( + prev: Record, + next: Record, + ) => boolean; + } + | undefined; + + const areEqual = memoInfo?.areEqual ?? shallowEqual; + if (areEqual(prevProps, nextProps)) { + return bailoutOnAlreadyFinishedWork( + current, + workInProgress, + renderLanes, + ); + } + } + } + + // Get the inner component from __memo or type.type + const memoInfo = (workInProgress.type as unknown as Record)[ + "__memo" + ] as { Component: FunctionalComponent } | undefined; + const type = workInProgress.type as { type?: FunctionalComponent }; + const resolvedType = + memoInfo?.Component ?? + type.type ?? + (Component as unknown as FunctionalComponent); + + return updateFunctionComponent( + current, + workInProgress, + resolvedType, + nextProps, + renderLanes, + ); +} + +// ============================================ +// Child Reconciliation +// ============================================ + +/** + * Reconciles children for a fiber. + */ +function reconcileChildren( + current: Fiber | null, + workInProgress: Fiber, + nextChildren: AnyMiniReactElement | undefined | null, + renderLanes: Lanes, +): void { + if (current === null) { + // Mount: no existing children + workInProgress.child = mountChildFibers( + workInProgress, + null, + nextChildren ?? null, + renderLanes, + ); + } else { + // Update: reconcile with existing children + workInProgress.child = reconcileChildFibers( + workInProgress, + current.child, + nextChildren ?? null, + renderLanes, + ); + } +} + +// ============================================ +// Bailout Logic +// ============================================ + +/** + * Checks if a fiber has scheduled work at the given lanes. + */ +function hasScheduledWork(fiber: Fiber, lanes: Lanes): boolean { + return ((fiber.lanes as number) & (lanes as number)) !== 0; +} + +/** + * Bails out of work when there's nothing to do. + */ +function bailoutOnAlreadyFinishedWork( + current: Fiber | null, + workInProgress: Fiber, + renderLanes: Lanes, +): Fiber | null { + // Check if children have work + if (!hasScheduledWorkInChildren(workInProgress, renderLanes)) { + // No work in the entire subtree + return null; + } + + // Clone child fibers + cloneChildFibersIfNeeded(current, workInProgress); + + return workInProgress.child; +} + +/** + * Checks if any children have scheduled work. + */ +function hasScheduledWorkInChildren(fiber: Fiber, lanes: Lanes): boolean { + return ((fiber.childLanes as number) & (lanes as number)) !== 0; +} + +/** + * Clones child fibers when bailing out. + * This creates WIP versions of children so they can be processed. + */ +function cloneChildFibersIfNeeded( + _current: Fiber | null, + workInProgress: Fiber, +): void { + if (workInProgress.child === null) { + return; + } + + // workInProgress.child currently points to current's children. + // We need to create WIP clones so the work loop can process them. + let currentChild: Fiber | null = workInProgress.child; + let prevNewFiber: Fiber | null = null; + let firstNewFiber: Fiber | null = null; + + while (currentChild !== null) { + // Create WIP clone of this child + let newFiber = currentChild.alternate; + if (newFiber === null) { + // Create new WIP fiber + newFiber = { + tag: currentChild.tag, + key: currentChild.key, + elementType: currentChild.elementType, + type: currentChild.type, + stateNode: currentChild.stateNode, + return: workInProgress, + child: currentChild.child, + sibling: null, + index: currentChild.index, + ref: currentChild.ref, + refCleanup: currentChild.refCleanup, + pendingProps: currentChild.memoizedProps ?? {}, + memoizedProps: currentChild.memoizedProps, + memoizedState: currentChild.memoizedState, + updateQueue: currentChild.updateQueue, + dependencies: currentChild.dependencies, + flags: NoFlags, + subtreeFlags: NoFlags, + deletions: null, + lanes: currentChild.lanes, + childLanes: currentChild.childLanes, + alternate: currentChild, + }; + currentChild.alternate = newFiber; + } else { + // Reuse existing WIP fiber + newFiber.pendingProps = currentChild.memoizedProps ?? {}; + newFiber.type = currentChild.type; + newFiber.flags = NoFlags; + newFiber.subtreeFlags = NoFlags; + newFiber.deletions = null; + newFiber.return = workInProgress; + newFiber.child = currentChild.child; + newFiber.memoizedProps = currentChild.memoizedProps; + newFiber.memoizedState = currentChild.memoizedState; + newFiber.updateQueue = currentChild.updateQueue; + newFiber.lanes = currentChild.lanes; + newFiber.childLanes = currentChild.childLanes; + } + + if (prevNewFiber === null) { + firstNewFiber = newFiber; + } else { + prevNewFiber.sibling = newFiber; + } + prevNewFiber = newFiber; + currentChild = currentChild.sibling; + } + + workInProgress.child = firstNewFiber; +} + +// ============================================ +// Utility Functions +// ============================================ + +/** + * Shallow equality check for props. + * Handles the special case of empty children arrays that are always + * new references from createElement. + */ +function shallowEqual( + objA: Record, + objB: Record, +): boolean { + if (objA === objB) { + return true; + } + + const keysA = Object.keys(objA); + const keysB = Object.keys(objB); + + if (keysA.length !== keysB.length) { + return false; + } + + for (const key of keysA) { + if (!Object.prototype.hasOwnProperty.call(objB, key)) { + return false; + } + + const valA = objA[key]; + const valB = objB[key]; + + if (valA !== valB) { + // Treat two empty arrays as equal (createElement always creates new children arrays) + if ( + Array.isArray(valA) && + Array.isArray(valB) && + valA.length === 0 && + valB.length === 0 + ) { + continue; + } + return false; + } + } + + return true; +} diff --git a/src/fiber/completeWork.ts b/src/fiber/completeWork.ts new file mode 100644 index 0000000..7261321 --- /dev/null +++ b/src/fiber/completeWork.ts @@ -0,0 +1,331 @@ +/* **************** */ +/* Complete Work - Render Phase Exit */ +/* **************** */ + +/** + * Implements the "complete" phase of fiber reconciliation. + * This is where we create DOM nodes and bubble up effects. + */ + +import { eventSystem } from "../events/eventSystem"; +import { + createInstance, + createTextInstance, + finalizeInitialChildren, + prepareUpdate, +} from "./commitWork"; +import { + assertTextProps, + isHostComponentFiber, + isHostTextFiber, + isTextProps, +} from "./typeGuards"; +import type { Fiber, Lanes } from "./types"; +import { NoFlags, Ref, Update, WorkTag, createFlags } from "./types"; + +// ============================================ +// Complete Work Entry Point +// ============================================ + +/** + * Performs the "complete" phase of work on a fiber. + * Called when we're done with a fiber and all its children. + */ +export function completeWork( + current: Fiber | null, + workInProgress: Fiber, + _renderLanes: Lanes, +): Fiber | null { + const newProps = workInProgress.pendingProps; + + switch (workInProgress.tag) { + case WorkTag.FunctionComponent: + bubbleProperties(workInProgress); + return null; + + case WorkTag.HostRoot: { + // Update the root's current pointer happens in commit + bubbleProperties(workInProgress); + return null; + } + + case WorkTag.HostComponent: { + const type = workInProgress.type as string; + + if (current !== null && workInProgress.stateNode !== null) { + // Update + updateHostComponent(current, workInProgress, type, newProps); + // Re-register the WIP fiber with the event system so event handlers + // pick up the new props + eventSystem.registerFiber( + workInProgress, + workInProgress.stateNode as Node, + ); + } else { + // Mount + if (!newProps) { + throw new Error("Expected host component props"); + } + + const instance = createInstance(type, newProps, workInProgress); + appendAllChildren(instance, workInProgress); + workInProgress.stateNode = instance; + + if (finalizeInitialChildren(instance, type, newProps)) { + markUpdate(workInProgress); + } + } + + bubbleProperties(workInProgress); + + // Handle ref + if (workInProgress.ref !== null) { + markRef(workInProgress); + } + + return null; + } + + case WorkTag.HostText: { + const textProps = assertTextProps(workInProgress); + const newText = textProps.nodeValue; + + if (current !== null && workInProgress.stateNode !== null) { + // Update + const oldProps = current.memoizedProps; + const oldText = isTextProps(oldProps) ? oldProps.nodeValue : undefined; + if (oldText !== newText) { + markUpdate(workInProgress); + } + } else { + // Mount + const textInstance = createTextInstance(newText); + workInProgress.stateNode = textInstance; + + // Register with event system for fiber-based event handling + eventSystem.registerFiber(workInProgress, textInstance); + } + + bubbleProperties(workInProgress); + return null; + } + + case WorkTag.Fragment: + bubbleProperties(workInProgress); + return null; + + case WorkTag.HostPortal: { + // Register event delegation for the portal container + const portalContainer = ( + workInProgress.stateNode as { containerInfo: Element } + ).containerInfo; + eventSystem.addEventDelegation(portalContainer); + bubbleProperties(workInProgress); + return null; + } + + case WorkTag.ContextProvider: + bubbleProperties(workInProgress); + return null; + + case WorkTag.ContextConsumer: + bubbleProperties(workInProgress); + return null; + + case WorkTag.MemoComponent: + bubbleProperties(workInProgress); + return null; + + default: + throw new Error(`Unknown fiber tag: ${workInProgress.tag}`); + } +} + +// ============================================ +// Host Component Operations +// ============================================ + +/** + * Updates a host component. + */ +function updateHostComponent( + current: Fiber, + workInProgress: Fiber, + type: string, + newProps: Record, +): void { + const oldProps = current.memoizedProps; + + if (oldProps === newProps) { + // No change + return; + } + + if (!isHostComponentFiber(workInProgress)) { + throw new Error("Expected HostComponent fiber in updateHostComponent"); + } + + const instance = workInProgress.stateNode; + const updatePayload = prepareUpdate(instance, type, oldProps ?? {}, newProps); + + if (updatePayload !== null) { + // Store the update payload for commit phase + // Note: updatePayload structure differs from UpdateQueue but is stored + // temporarily here for commit phase processing (follows React's pattern) + workInProgress.updateQueue = + updatePayload as unknown as Fiber["updateQueue"]; + markUpdate(workInProgress); + } +} + +/** + * Appends all children to a parent instance. + */ +function appendAllChildren(parent: Element, workInProgress: Fiber): void { + let node = workInProgress.child; + + while (node !== null) { + if (isHostComponentFiber(node) || isHostTextFiber(node)) { + parent.appendChild(node.stateNode); + } else if (node.tag === WorkTag.HostPortal) { + // Portals don't append to parent + } else if (node.child !== null) { + // Function components, fragments, etc - go into children + node.child.return = node; + node = node.child; + continue; + } + + if (node === workInProgress) { + return; + } + + while (node.sibling === null) { + if (node.return === null || node.return === workInProgress) { + return; + } + node = node.return; + } + + node.sibling.return = node.return; + node = node.sibling; + } +} + +// ============================================ +// Effect Flags +// ============================================ + +/** + * Marks a fiber as needing an update. + */ +function markUpdate(workInProgress: Fiber): void { + workInProgress.flags = createFlags( + (workInProgress.flags as number) | (Update as number), + ); +} + +/** + * Marks a fiber as having a ref. + */ +function markRef(workInProgress: Fiber): void { + workInProgress.flags = createFlags( + (workInProgress.flags as number) | (Ref as number), + ); +} + +// ============================================ +// Effect Bubbling +// ============================================ + +/** + * Bubbles properties (flags and lanes) up from children to parent. + */ +function bubbleProperties(completedWork: Fiber): void { + let subtreeFlags = NoFlags; + let child = completedWork.child; + + while (child !== null) { + subtreeFlags = createFlags( + (subtreeFlags as number) | + (child.subtreeFlags as number) | + (child.flags as number), + ); + + // Also merge child lanes + completedWork.childLanes = createFlags( + (completedWork.childLanes as number) | + (child.lanes as number) | + (child.childLanes as number), + ) as unknown as Lanes; + + child = child.sibling; + } + + completedWork.subtreeFlags = createFlags( + (completedWork.subtreeFlags as number) | (subtreeFlags as number), + ); +} + +// ============================================ +// Reset Operations +// ============================================ + +/** + * Resets the completed work for a new render pass. + */ +export function resetCompleteWork(workInProgress: Fiber): void { + workInProgress.stateNode = null; + workInProgress.flags = NoFlags; + workInProgress.subtreeFlags = NoFlags; +} + +// ============================================ +// Unwind Operations +// ============================================ + +/** + * Unwinds work when an error occurs. + */ +export function unwindWork( + _current: Fiber | null, + workInProgress: Fiber, + _renderLanes: Lanes, +): Fiber | null { + switch (workInProgress.tag) { + case WorkTag.HostRoot: + // Pop root context + return null; + case WorkTag.HostComponent: + // Pop host context + return null; + case WorkTag.HostPortal: + // Pop portal context + return null; + case WorkTag.ContextProvider: + // Pop provider context + return null; + default: + return null; + } +} + +/** + * Unwinds interrupted work. + */ +export function unwindInterruptedWork( + _current: Fiber | null, + interruptedWork: Fiber, + _renderLanes: Lanes, +): void { + switch (interruptedWork.tag) { + case WorkTag.HostRoot: + break; + case WorkTag.HostComponent: + break; + case WorkTag.HostPortal: + break; + case WorkTag.ContextProvider: + break; + } +}