···11import { FiberTag, EffectTag } from './fiber.ts';
22+import { cleanUpEffects } from './hooks';
23import { assert } from './utils.ts';
3445const {
···4950 let fiber = forFiber;
5051 while (true) {
5152 if (fiber.tag === HostFiber || fiber.tag === HostTextFiber) {
5252- // TODO: recursively unmount all child fibers
5353+ // There is no need to remove DOM nodes for ancestors of this node
5454+ // because the DOM will remove them for us, but we do still need to
5555+ // traverse the subtree and unmount its components,
5656+ // and that happens in `unmountFiberTree()`
5757+ unmountFiberTree(fiber);
53585459 // A host fiber which is being deleted must have already been placed,
5560 // so it must have a DOM node
5661 assert(fiber.dom);
5757- // It is sufficient to delete the DOM node without traversing the host fiber's children,
5858- // as the DOM will remove all DOM children for us
5962 domParent.removeChild(fiber.dom);
6063 }
6164 else {
6262- // TODO: unmount `fiber`
6565+ // Unlike the above Host case, it is okay to unmount only this fiber
6666+ // because we are going to traverse its children within this loop
6767+ unmountFiber(fiber);
63686469 // This is a non-host fiber so it has no DOM node, but its children might,
6570 // so we must traverse them looking for host fibers
···8994 // Continue with the next sibling (which must be a descendant of `forFiber`)
9095 fiber = fiber.sibling;
9196 }
9797+}
9898+9999+function unmountFiberTree(rootFiber) {
100100+ let fiber = rootFiber;
101101+ while (true) {
102102+ unmountFiber(fiber);
103103+104104+ if (fiber.child) {
105105+ fiber = fiber.child;
106106+ continue;
107107+ }
108108+ if (fiber === rootFiber) return;
109109+110110+ while (!fiber.sibling) {
111111+ if (!fiber.parent) return;
112112+ if (fiber.parent === rootFiber) return;
113113+ fiber = fiber.parent;
114114+ }
115115+ fiber = fiber.sibling;
116116+ }
117117+}
118118+119119+function unmountFiber(fiber) {
120120+ cleanUpEffects(fiber);
92121}
9312294123function commitTextPlacement(fiber) {
+15-8
js/fiber.ts
···11-import { assertUnreachable } from './utils.ts';
11+import { assert, assertUnreachable } from './utils.ts';
2233export const FiberTag = {
44 RootFiber: 'root',
···991010export const EffectTag = {
1111 Placement: 'placement',
1212+ Delete: 'delete',
1213};
13141415const {
···20212122const {
2223 Placement,
2424+ Delete,
2325} = EffectTag;
24262527let nextFiberId = 0;
···9496 if (!childElementOrElements) return;
95979698 if (Array.isArray(childElementOrElements)) {
9797- // TODO: is flattening correct?
9899 const elementsFlat = childElementOrElements.flat();
99100 return reconcileArray(parentFiber, elementsFlat);
100101 }
···104105}
105106106107function reconcileArray(parentFiber, elements) {
108108+ assert(parentFiber.effect !== Delete);
107109 const oldFiberLookup = buildLookup(parentFiber.child);
108110109111 parentFiber.deletedChildren = [];
···125127 if (newFiber.alternate) {
126128 const oldIndex = oldFiber.index;
127129 if (oldIndex < lastKeptIndex) {
128128- newFiber.effect = 'placement';
130130+ newFiber.effect = Placement;
129131 }
130132 else {
131133 lastKeptIndex = i;
132134 }
133135 }
134136 else {
135135- newFiber.effect = 'placement';
137137+ newFiber.effect = Placement;
136138 }
137137- if (oldFiber && oldFiber.effect === 'delete') {
139139+ if (oldFiber && oldFiber.effect === Delete) {
138140 parentFiber.deletedChildren.push(oldFiber);
139141 }
140142···142144 else firstNewChild = newFiber;
143145 prevChild = newFiber;
144146 }
147147+ // Remove any existing sibling pointer from the last child
148148+ // TODO: maybe this should be done by cloneFiber,
149149+ // it doesn't seem right to have to worry about it here
150150+ // (and it has already caused a bug)
151151+ if (prevChild) prevChild.sibling = null;
145152146153 oldFiberLookup.forEach(deadFiber => {
147147- deadFiber.effect = 'delete';
154154+ deadFiber.effect = Delete;
148155 parentFiber.deletedChildren.push(deadFiber);
149156 });
150157···165172 const nodeValue = element.toString();
166173167174 if (oldFiber === null || oldFiber.tag !== HostTextFiber) {
168168- if (oldFiber) oldFiber.effect = 'delete';
175175+ if (oldFiber) oldFiber.effect = Delete;
169176 const newFiber = makeFiber(HostTextFiber);
170177 newFiber.props = { nodeValue };
171178 return newFiber;
···179186180187function updateElement(oldFiber, element, index) {
181188 if (oldFiber === null || oldFiber.type !== element.type) {
182182- if (oldFiber) oldFiber.effect = 'delete';
189189+ if (oldFiber) oldFiber.effect = Delete;
183190 const newFiber = makeFiber(fiberTagForType(element.type));
184191 newFiber.type = element.type;
185192 newFiber.props = element.props;
+13-1
js/hooks.ts
···1212 Effect,
1313} = Hook;
14141515-function useFiber() {
1515+export function useFiber() {
1616 const currentFiber = getCurrentFiber();
1717 if (!currentFiber) throw new Error('Hooks can only be called from within a function component.');
1818 return currentFiber;
···7272 const newCleanup = callback();
7373 fiber.hooks[pointer + 1] = newCleanup;
7474 });
7575+ }
7676+}
7777+7878+export function cleanUpEffects(fiber) {
7979+ const hooks = fiber.hooks;
8080+ if (!hooks) return;
8181+8282+ for (let i = 0; i < hooks.length; i += 3) {
8383+ if (hooks[i] === Effect) {
8484+ const cleanup = hooks[i + 1];
8585+ cleanup();
8686+ }
7587 }
7688}
7789