this repo has no description
1import { FiberTag, EffectTag } from './fiber.ts';
2import { cleanUpEffects } from './hooks';
3import { assert } from './utils.ts';
4
5const {
6 HostFiber,
7 HostTextFiber,
8} = FiberTag;
9
10const {
11 Placement,
12} = EffectTag;
13
14export function commitTree(fiber) {
15 fiber.current = true;
16 if (fiber.alternate) fiber.alternate.current = false;
17
18 if (fiber.child) commit(fiber.child);
19}
20
21function commit(fiber) {
22 commitWork(fiber);
23
24 if (fiber.child) commit(fiber.child);
25 if (fiber.sibling) commit(fiber.sibling);
26}
27
28function commitWork(fiber) {
29 fiber.current = true;
30 if (fiber.alternate) fiber.alternate.current = false;
31
32 if (fiber.tag == HostTextFiber) {
33 if (fiber.effect === 'placement') commitTextPlacement(fiber);
34 else commitTextUpdate(fiber);
35 }
36 else if (fiber.tag == HostFiber) {
37 if (fiber.effect === 'placement') commitElementPlacement(fiber);
38 else commitElementUpdate(fiber);
39 }
40
41 if (fiber.deletedChildren) {
42 for (const deleteChild of fiber.deletedChildren) {
43 commitDeletion(deleteChild);
44 }
45 }
46}
47
48function commitDeletion(fiber) {
49 const domParent = findDomParent(fiber);
50 removeAllChildren(domParent, fiber);
51}
52
53function removeAllChildren(domParent, forFiber) {
54 let fiber = forFiber;
55 while (true) {
56 if (fiber.tag === HostFiber || fiber.tag === HostTextFiber) {
57 // There is no need to remove DOM nodes for ancestors of this node
58 // because the DOM will remove them for us, but we do still need to
59 // traverse the subtree and unmount its components,
60 // and that happens in `unmountFiberTree()`
61 unmountFiberTree(fiber);
62
63 // A host fiber which is being deleted must have already been placed,
64 // so it must have a DOM node
65 assert(fiber.dom);
66 domParent.removeChild(fiber.dom);
67 }
68 else {
69 // Unlike the above Host case, it is okay to unmount only this fiber
70 // because we are going to traverse its children within this loop
71 unmountFiber(fiber);
72
73 // This is a non-host fiber so it has no DOM node, but its children might,
74 // so we must traverse them looking for host fibers
75 if (fiber.child) {
76 fiber = fiber.child;
77 continue;
78 }
79 }
80
81 // We have reached the original `forFiber` (or it had no children),
82 // so we're done
83 if (fiber === forFiber) return;
84
85 // At this point we must be within a descendant of `forFiber`
86 //
87 // If this (child) fiber has no sibling we must go up the tree until we find
88 // a parent with a sibling
89 // (this "parent" will still be a descendant of `forFiber`)
90 while (!fiber.sibling) {
91 // If we hit the root, we must be done
92 if (!fiber.parent) return;
93 // If we hit the original `forFiber`, we must be done
94 if (fiber.parent === forFiber) return;
95
96 fiber = fiber.parent;
97 }
98 // Continue with the next sibling (which must be a descendant of `forFiber`)
99 fiber = fiber.sibling;
100 }
101}
102
103function unmountFiberTree(rootFiber) {
104 let fiber = rootFiber;
105 while (true) {
106 unmountFiber(fiber);
107
108 if (fiber.child) {
109 fiber = fiber.child;
110 continue;
111 }
112 if (fiber === rootFiber) return;
113
114 while (!fiber.sibling) {
115 if (!fiber.parent) return;
116 if (fiber.parent === rootFiber) return;
117 fiber = fiber.parent;
118 }
119 fiber = fiber.sibling;
120 }
121}
122
123function unmountFiber(fiber) {
124 cleanUpEffects(fiber);
125
126 fiber.current = false;
127 if (fiber.alternate) assert(fiber.alternate.current === false);
128}
129
130function commitTextPlacement(fiber) {
131 const domParent = findDomParent(fiber);
132 const domSibling = findNextExistingDomSibling(fiber);
133
134 const domNode = document.createTextNode(fiber.props.nodeValue);
135 fiber.dom = domNode;
136
137 if (domSibling) domParent.insertBefore(domNode, domSibling);
138 else domParent.appendChild(domNode, domSibling);
139}
140
141function commitTextUpdate(fiber) {
142 const domNode = fiber.dom;
143 assert(domNode.nodeType === Node.TEXT_NODE);
144
145 // TODO: diff during reconciliation?
146 const oldValue = fiber.alternate.props.nodeValue;
147 const newValue = fiber.props.nodeValue;
148 if (newValue !== oldValue) domNode.nodeValue = newValue;
149}
150
151function commitElementPlacement(fiber) {
152 const domParent = findDomParent(fiber);
153 const domSibling = findNextExistingDomSibling(fiber);
154
155 const domNode = document.createElement(fiber.type);
156 fiber.dom = domNode;
157
158 if (domSibling) domParent.insertBefore(domNode, domSibling);
159 else domParent.appendChild(domNode);
160
161 diffProps(fiber);
162}
163
164function commitElementUpdate(fiber) {
165 diffProps(fiber);
166}
167
168function diffProps(fiber) {
169 const domNode = fiber.dom;
170 assert(domNode);
171
172 // TODO: diff during reconciliation?
173 const oldProps = fiber.alternate ? fiber.alternate.props : {};
174 const newProps = fiber.props;
175
176 // Build lookup for old props
177 const oldLookup = new Map();
178 for (const [key, oldVal] of Object.entries(oldProps)) {
179 if (key === 'children') continue;
180 oldLookup.set(key, oldVal);
181 }
182
183 // Diff new props, removing keys from oldLookup if present
184 for (const [key, newVal] of Object.entries(newProps)) {
185 if (key === 'children') continue;
186
187 const oldVal = oldLookup.get(key);
188 oldLookup.delete(key);
189
190 if (isEventPropKey(key)) {
191 const eventName = eventForPropKey(key);
192
193 if (oldVal !== undefined) domNode.removeEventListener(eventName, oldVal);
194 domNode.addEventListener(eventName, newVal);
195 }
196 else {
197 if (newVal !== oldVal) {
198 if (fiber.type === 'input' && key === 'value') {
199 if (newVal === false || newVal === null || newVal === undefined) domNode.value = null;
200 else domNode.value = newVal;
201 }
202 else if (fiber.type === 'input' && key === 'checked') {
203 if (newVal === false || newVal === null || newVal === undefined) domNode.checked = false;
204 else domNode.checked = true;
205 }
206 else{
207 if (newVal === false || newVal === null || newVal === undefined) domNode.removeAttribute(key);
208 else if (newVal === true) domNode.setAttribute(key, '');
209 else domNode.setAttribute(key, newVal);
210 }
211 }
212 }
213 }
214
215 // Remove remaining old props that were not found in newProps
216 oldLookup.forEach((oldVal, key) => {
217 if (isEventPropKey(key)) domNode.removeEventListener(eventForPropKey(key), oldVal);
218 else domNode.removeAttribute(key);
219 });
220}
221
222function isEventPropKey(key) {
223 return key.startsWith('on');
224}
225
226function eventForPropKey(key) {
227 return key.substring(2).toLowerCase();
228}
229
230function findDomParent(forFiber) {
231 let fiber = forFiber.parent;
232 while (fiber) {
233 if (fiber.dom) return fiber.dom;
234 fiber = fiber.parent;
235 }
236 throw new Error(`Could not find parent for fiber ${forFiber}`);
237}
238
239// Find the next DOM sibling for `forFiber` which already exists (from the last render)
240//
241// Naively this could be done by simply traversing the tree siblings of the fiber (`fiber.sibling`),
242// however non-host (function component) fibers do not have DOM nodes, so the DOM sibling of a
243// given fiber may be above *or* below it in the fiber tree
244function findNextExistingDomSibling(forFiber) {
245 let fiber = forFiber;
246
247 siblingIter: while (true) {
248 // If the current node has no siblings, traverse up the tree
249 // looking for DOM siblings
250 while(!fiber.sibling) {
251 // If we hit the root (no parent) then there are no more nodes to check
252 if (!fiber.parent) return null;
253 // If we hit a Host fiber, that must the the DOM parent of `forFiber`,
254 // so there are no more siblings to check
255 // (we do not check for a HostTextFiber because text nodes cannot be parents)
256 if (fiber.parent.tag == HostFiber) return null;
257 assert(fiber.parent.tag !== HostTextFiber);
258
259 fiber = fiber.parent;
260 }
261
262 fiber = fiber.sibling;
263 // If `fiber` is a non-host fiber, traverse down the tree to find DOM nodes within it
264 while (fiber.tag !== HostFiber && fiber.tag !== HostTextFiber) {
265 // If this (non-host) sibling is being placed during this render, it cannot have DOM children,
266 // so we continue to the next sibling
267 if (fiber.effect === Placement) continue siblingIter;
268 // If this (non-host) sibling has no children we continue to the next sibling
269 if (!fiber.child) continue siblingIter;
270
271 fiber = fiber.child;
272 }
273
274 // At this point `fiber` must be a Host sibling
275 //
276 // If `fiber` is being placed during this render it cannot have a DOM node,
277 // so we continue to the next sibling
278 if (fiber.effect === Placement) continue;
279
280 assert(fiber.dom);
281 return fiber.dom;
282 }
283}