Skip to content

Commit e05a1e6

Browse files
committed
feat: add commit phase (commitWork + commitRoot)
Add commitPlacement, commitUpdate, commitDeletion for DOM mutations. Add commitRoot orchestrating the three commit sub-phases: before mutation, mutation, and layout.
1 parent 6d6c174 commit e05a1e6

2 files changed

Lines changed: 939 additions & 0 deletions

File tree

src/fiber/commitRoot.ts

Lines changed: 351 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,351 @@
1+
/* **************** */
2+
/* Commit Root - Phase Orchestration */
3+
/* **************** */
4+
5+
/**
6+
* Implements the commit phase of the fiber reconciler.
7+
* The commit phase is synchronous and cannot be interrupted.
8+
*
9+
* Three sub-phases:
10+
* 1. Before Mutation - Read DOM layout (getSnapshotBeforeUpdate)
11+
* 2. Mutation - Perform DOM mutations
12+
* 3. Layout - Run layout effects and refs (synchronously)
13+
*
14+
* Passive effects (useEffect) are scheduled asynchronously.
15+
*/
16+
17+
import {
18+
commitAttachRef,
19+
commitDeletion,
20+
commitDetachRef,
21+
commitPlacement,
22+
commitUpdate,
23+
} from "./commitWork";
24+
import {
25+
collectEffects,
26+
collectEffectsWithCleanup,
27+
doesFiberHavePassiveEffects,
28+
runLayoutEffectCleanups,
29+
runLayoutEffectCreates,
30+
schedulePassiveEffects,
31+
} from "./effectList";
32+
import { markRootFinished } from "./lanes";
33+
import type { Fiber, FiberRoot, Lanes } from "./types";
34+
import {
35+
ChildDeletion,
36+
LayoutMask,
37+
MutationMask,
38+
Placement,
39+
Ref,
40+
Snapshot,
41+
Update,
42+
WorkTag,
43+
createFlags,
44+
} from "./types";
45+
46+
// ============================================
47+
// Commit Root Entry Point
48+
// ============================================
49+
50+
/**
51+
* Commits the finished work to the DOM.
52+
* This is the main entry point for the commit phase.
53+
*/
54+
export function commitRoot(root: FiberRoot): void {
55+
const finishedWork = root.finishedWork;
56+
const finishedLanes = root.finishedLanes;
57+
if (finishedWork === null) {
58+
return;
59+
}
60+
61+
// Save reference to current tree (for effect cleanup)
62+
const previousCurrent = root.current;
63+
64+
// Clear the finished work
65+
root.finishedWork = null;
66+
root.finishedLanes = 0 as Lanes;
67+
68+
// Check if there are any passive effects in either tree
69+
const hasPassiveEffectsInNew = doesFiberHavePassiveEffects(finishedWork);
70+
const hasPassiveEffectsInOld = doesFiberHavePassiveEffects(previousCurrent);
71+
const hasPassiveEffects = hasPassiveEffectsInNew || hasPassiveEffectsInOld;
72+
73+
// === Before Mutation Phase ===
74+
commitBeforeMutationEffects(root, finishedWork);
75+
76+
// === Mutation Phase ===
77+
commitMutationEffects(root, finishedWork);
78+
79+
// Switch the current tree pointer
80+
root.current = finishedWork;
81+
82+
// === Layout Phase ===
83+
commitLayoutEffects(root, finishedWork);
84+
85+
// Mark the lanes as finished
86+
markRootFinished(root, finishedLanes);
87+
88+
// === Schedule Passive Effects ===
89+
if (hasPassiveEffects) {
90+
// Collect effects from new tree (for creates) and old tree (for cleanups)
91+
const effects = collectEffectsWithCleanup(finishedWork, previousCurrent);
92+
schedulePassiveEffects(effects);
93+
}
94+
}
95+
96+
// ============================================
97+
// Before Mutation Phase
98+
// ============================================
99+
100+
/**
101+
* Phase 1: Before Mutation
102+
* Called before the DOM is mutated.
103+
* Used for reading DOM state (e.g., getSnapshotBeforeUpdate).
104+
*/
105+
function commitBeforeMutationEffects(
106+
_root: FiberRoot,
107+
finishedWork: Fiber,
108+
): void {
109+
commitBeforeMutationEffectsOnFiber(finishedWork);
110+
}
111+
112+
/**
113+
* Recursively processes before mutation effects.
114+
* Uses DFS traversal visiting each fiber exactly once.
115+
*/
116+
function commitBeforeMutationEffectsOnFiber(fiber: Fiber): void {
117+
const flags = fiber.flags;
118+
119+
// Process snapshot effects
120+
if ((flags as number) & (Snapshot as number)) {
121+
switch (fiber.tag) {
122+
case WorkTag.FunctionComponent:
123+
// Function components don't have getSnapshotBeforeUpdate
124+
break;
125+
case WorkTag.HostRoot:
126+
// Could clear container for portal root
127+
break;
128+
case WorkTag.HostComponent:
129+
// Host components don't need snapshots in our implementation
130+
break;
131+
}
132+
}
133+
134+
// Recurse into child (child will handle its own siblings via recursion)
135+
if (fiber.child !== null) {
136+
commitBeforeMutationEffectsOnFiber(fiber.child);
137+
}
138+
139+
// Then move to sibling (avoiding duplicate iteration)
140+
if (fiber.sibling !== null) {
141+
commitBeforeMutationEffectsOnFiber(fiber.sibling);
142+
}
143+
}
144+
145+
// ============================================
146+
// Mutation Phase
147+
// ============================================
148+
149+
/**
150+
* Phase 2: Mutation
151+
* Performs actual DOM mutations: placements, updates, deletions.
152+
*/
153+
function commitMutationEffects(root: FiberRoot, finishedWork: Fiber): void {
154+
commitMutationEffectsOnFiber(root, finishedWork);
155+
}
156+
157+
/**
158+
* Recursively processes mutation effects.
159+
* Uses DFS traversal visiting each fiber exactly once.
160+
*/
161+
function commitMutationEffectsOnFiber(root: FiberRoot, fiber: Fiber): void {
162+
const flags = fiber.flags;
163+
164+
// Handle deletions first
165+
if ((flags as number) & (ChildDeletion as number)) {
166+
const deletions = fiber.deletions;
167+
if (deletions !== null) {
168+
for (const childToDelete of deletions) {
169+
commitDeletion(root, childToDelete, 0);
170+
}
171+
}
172+
}
173+
174+
// Recurse into children (child will handle its own siblings via recursion)
175+
if (fiber.child !== null) {
176+
commitMutationEffectsOnFiber(root, fiber.child);
177+
}
178+
179+
// Process this fiber's mutations
180+
if ((flags as number) & (MutationMask as number)) {
181+
// Detach ref before mutations
182+
if ((flags as number) & (Ref as number)) {
183+
const current = fiber.alternate;
184+
if (current !== null) {
185+
commitDetachRef(current);
186+
}
187+
}
188+
189+
// Handle placement
190+
if ((flags as number) & (Placement as number)) {
191+
commitPlacement(fiber);
192+
// Clear the placement flag
193+
fiber.flags = createFlags(
194+
(fiber.flags as number) & ~(Placement as number),
195+
);
196+
}
197+
198+
// Handle update
199+
if ((flags as number) & (Update as number)) {
200+
commitUpdate(fiber);
201+
}
202+
}
203+
204+
// Then move to sibling (avoiding duplicate iteration)
205+
if (fiber.sibling !== null) {
206+
commitMutationEffectsOnFiber(root, fiber.sibling);
207+
}
208+
}
209+
210+
// ============================================
211+
// Layout Phase
212+
// ============================================
213+
214+
/**
215+
* Phase 3: Layout
216+
* Called after DOM mutations.
217+
* Runs layout effects (useLayoutEffect) and attaches refs.
218+
*/
219+
function commitLayoutEffects(root: FiberRoot, finishedWork: Fiber): void {
220+
commitLayoutEffectsOnFiber(root, finishedWork);
221+
}
222+
223+
/**
224+
* Recursively processes layout effects.
225+
* Uses DFS traversal visiting each fiber exactly once.
226+
*/
227+
function commitLayoutEffectsOnFiber(root: FiberRoot, fiber: Fiber): void {
228+
const flags = fiber.flags;
229+
230+
// Recurse into children first (child will handle its own siblings via recursion)
231+
if (fiber.child !== null) {
232+
commitLayoutEffectsOnFiber(root, fiber.child);
233+
}
234+
235+
// Process this fiber's layout effects
236+
if ((flags as number) & (LayoutMask as number)) {
237+
// Run layout effects for function components
238+
if (fiber.tag === WorkTag.FunctionComponent) {
239+
commitLayoutEffectOnFunctionComponent(fiber);
240+
}
241+
242+
// Attach ref after DOM is ready
243+
if ((flags as number) & (Ref as number)) {
244+
commitAttachRef(fiber);
245+
}
246+
}
247+
248+
// Then move to sibling (avoiding duplicate iteration)
249+
if (fiber.sibling !== null) {
250+
commitLayoutEffectsOnFiber(root, fiber.sibling);
251+
}
252+
}
253+
254+
/**
255+
* Commits layout effects for a function component.
256+
*/
257+
function commitLayoutEffectOnFunctionComponent(_fiber: Fiber): void {
258+
// Layout effects are collected and run together
259+
// The actual effect execution happens via collectEffects/runLayoutEffects
260+
// This function can be used for component-specific handling
261+
}
262+
263+
// ============================================
264+
// Effect Execution Helpers
265+
// ============================================
266+
267+
/**
268+
* Commits all pending layout effects synchronously.
269+
*/
270+
export function flushLayoutEffects(finishedWork: Fiber): void {
271+
const effects = collectEffects(finishedWork);
272+
runLayoutEffectCleanups(effects.layoutEffects);
273+
runLayoutEffectCreates(effects.layoutEffects);
274+
}
275+
276+
/**
277+
* Commits unmount effects for a fiber tree.
278+
*/
279+
export function commitUnmountEffects(fiber: Fiber): void {
280+
let node: Fiber | null = fiber;
281+
282+
while (node !== null) {
283+
// Clean up refs
284+
const ref = node.ref;
285+
if (ref !== null) {
286+
if (typeof ref === "function") {
287+
try {
288+
ref(null);
289+
} catch (error) {
290+
console.error("Error in ref cleanup:", error);
291+
}
292+
} else if ("current" in ref) {
293+
ref.current = null;
294+
}
295+
}
296+
297+
// Traverse the tree
298+
if (node.child !== null) {
299+
node = node.child;
300+
continue;
301+
}
302+
303+
if (node === fiber) {
304+
return;
305+
}
306+
307+
while (node.sibling === null) {
308+
if (node.return === null || node.return === fiber) {
309+
return;
310+
}
311+
node = node.return;
312+
}
313+
314+
node = node.sibling;
315+
}
316+
}
317+
318+
// ============================================
319+
// Root Operations
320+
// ============================================
321+
322+
/**
323+
* Prepares the root for commit.
324+
* Called before starting the commit phase.
325+
*/
326+
export function prepareForCommit(_containerInfo: Element): void {
327+
// Could disable events during commit for consistency
328+
// For now, this is a no-op
329+
}
330+
331+
/**
332+
* Resets after commit.
333+
* Called after the commit phase completes.
334+
*/
335+
export function resetAfterCommit(_containerInfo: Element): void {
336+
// Could re-enable events after commit
337+
// For now, this is a no-op
338+
}
339+
340+
// ============================================
341+
// Deletion Queue
342+
// ============================================
343+
344+
/**
345+
* Processes all pending deletions.
346+
*/
347+
export function commitDeletions(root: FiberRoot, deletions: Fiber[]): void {
348+
for (const fiber of deletions) {
349+
commitDeletion(root, fiber, 0);
350+
}
351+
}

0 commit comments

Comments
 (0)