this repo has no description
at master 283 lines 8.6 kB view raw
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}