@samitouri / QOS-React-2 / commits / edaf08fcfe

Fiber child reconciliation

This implements the first step to proper child reconciliation. It doesn't yet track side-effects like insert/move/delete but has the main reconciliation algorithm in place. The goal of this algorithm is to branch early and avoid rechecking those conditions. That leads to some duplications of code. There are three major branches: - Reconciling a single child per type. - Reconciling all children that are in the same slot as before from the beginning. - Adding remaining children to a temporary Map and reconciling them by scanning the map. Even when we use the Map strategy we have to scan the linked list to line up "same slot" positions because React, unlike Inferno, can have implicit keys interleaved with explicit keys.

Sebastian Markbage committed Sep 19, 2016 at 20:53 UTC edaf08fcfe179b8ac9742bcb60791f42024b8d0d
3 files changed +660 -157
src/renderers/shared/fiber/ReactChildFiber.js
+639 -155
@@ -16,8 +16,6 @@ import type { ReactCoroutine, ReactYield } from 'ReactCoroutine';
16 import type { Fiber } from 'ReactFiber';
17 import type { PriorityLevel } from 'ReactPriorityLevel';
18
19 -import type { ReactNodeList } from 'ReactTypes';
20 -
19 var REACT_ELEMENT_TYPE = require('ReactElementSymbol');
20 var {
21 REACT_COROUTINE_TYPE,
@@ -25,7 +23,11 @@ var {
23 } = require('ReactCoroutine');
24
25 var ReactFiber = require('ReactFiber');
26 +var ReactPriorityLevel = require('ReactPriorityLevel');
27 var ReactReifiedYield = require('ReactReifiedYield');
28 +var ReactTypeOfWork = require('ReactTypeOfWork');
29 +
30 +var getIteratorFn = require('getIteratorFn');
31
32 const {
33 cloneFiber,
@@ -38,211 +40,683 @@ const {
40
41 const {
42 createReifiedYield,
43 + createUpdatedReifiedYield,
44 } = ReactReifiedYield;
45
46 const isArray = Array.isArray;
47
45 -function ChildReconciler(shouldClone) {
48 +const {
49 + HostText,
50 + CoroutineComponent,
51 + YieldComponent,
52 + Fragment,
53 +} = ReactTypeOfWork;
54 +
55 +const {
56 + NoWork,
57 +} = ReactPriorityLevel;
58 +
59 +
60 +function deleteChild(
61 + returnFiber : Fiber,
62 + childToDelete : Fiber
63 +) {
64 + // TODO: Add this child to the side-effect queue for deletion.
65 +}
66 +
67 +function deleteRemainingChildren(
68 + returnFiber : Fiber,
69 + currentFirstChild : ?Fiber
70 +) {
71 + // TODO: Add these children to the side-effect queue for deletion.
72 + return null;
73 +}
74 +
75 +function mapAndDeleteRemainingChildren(
76 + returnFiber : Fiber,
77 + currentFirstChild : ?Fiber
78 +) : Map<string, Fiber> {
79 + // Add the remaining children to a temporary map so that we can find them by
80 + // keys quickly. At the same time, we'll flag them all for deletion. However,
81 + // we will then undo the deletion as we restore children. Implicit (null) keys
82 + // don't get added to this set.
83 + const existingChildren : Map<string, Fiber> = new Map();
84 + // TODO: This also needs to store the "previous index of this node". That lets
85 + // us determine whether something needs to be a placement. It might be best to
86 + // just store this on the fiber itself since that lets us use the "implicit"
87 + // index resolution mechanism without adding null values to the linked list.
88 + let existingChild = currentFirstChild;
89 + while (existingChild) {
90 + if (existingChild.key !== null) {
91 + existingChildren.set(existingChild.key, existingChild);
92 + }
93 + // Add everything to the delete queue
94 + // Actually... It is not possible to delete things from the queue since
95 + // we don't have access to the previous link. Does that mean we need a
96 + // second pass to add them? We should be able to keep track of the
97 + // previous deletion as we're iterating through the list the next time.
98 + // That way we know which item to patch when we delete a deletion.
99 + existingChild = existingChild.sibling;
100 + }
101 + return existingChildren;
102 +}
103 +
104 +
105 +// This wrapper function exists because I expect to clone the code in each path
106 +// to be able to optimize each path individually by branching early. This needs
107 +// a compiler or we can do it manually. Helpers that don't need this branching
108 +// live outside of this function.
109 +function ChildReconciler(shouldClone, shouldTrackSideEffects) {
110 +
111 + function useFiber(fiber : Fiber, priority : PriorityLevel) {
112 + // We currently set sibling to null here because it is easy to forget to do
113 + // before returning it.
114 + if (shouldClone) {
115 + const clone = cloneFiber(fiber, priority);
116 + clone.sibling = null;
117 + return clone;
118 + } else {
119 + if (fiber.pendingWorkPriority === NoWork ||
120 + fiber.pendingWorkPriority > priority) {
121 + fiber.pendingWorkPriority = priority;
122 + }
123 + fiber.sibling = null;
124 + return fiber;
125 + }
126 + }
127 +
128 + function updateTextNode(
129 + returnFiber : Fiber,
130 + current : ?Fiber,
131 + textContent : string,
132 + priority : PriorityLevel
133 + ) {
134 + if (current == null || current.tag !== HostText) {
135 + // Insert
136 + const created = createFiberFromText(textContent, priority);
137 + created.return = returnFiber;
138 + return created;
139 + } else {
140 + // Update
141 + const existing = useFiber(current, priority);
142 + existing.pendingProps = textContent;
143 + existing.return = returnFiber;
144 + return existing;
145 + }
146 + }
147
47 - function createSubsequentChild(
148 + function updateElement(
149 returnFiber : Fiber,
49 - existingChild : ?Fiber,
50 - previousSibling : Fiber,
51 - newChildren : any,
150 + current : ?Fiber,
151 + element : ReactElement<any>,
152 priority : PriorityLevel
53 - ) : Fiber {
54 - if (typeof newChildren === 'string') {
55 - const textNode = createFiberFromText(newChildren, priority);
56 - previousSibling.sibling = textNode;
57 - textNode.return = returnFiber;
58 - return textNode;
153 + ) {
154 + if (current == null || current.type !== element.type) {
155 + // Insert
156 + const created = createFiberFromElement(element, priority);
157 + created.return = returnFiber;
158 + return created;
159 + } else {
160 + // Move based on index, TODO: This needs to restore a deletion marking.
161 + const existing = useFiber(current, priority);
162 + existing.pendingProps = element.props;
163 + existing.return = returnFiber;
164 + return existing;
165 }
166 + }
167
61 - if (typeof newChildren !== 'object' || newChildren === null) {
62 - return previousSibling;
168 + function updateCoroutine(
169 + returnFiber : Fiber,
170 + current : ?Fiber,
171 + coroutine : ReactCoroutine,
172 + priority : PriorityLevel
173 + ) {
174 + // TODO: Should this also compare handler to determine whether to reuse?
175 + if (current == null || current.tag !== CoroutineComponent) {
176 + // Insert
177 + const created = createFiberFromCoroutine(coroutine, priority);
178 + created.return = returnFiber;
179 + return created;
180 + } else {
181 + // Move based on index, TODO: This needs to restore a deletion marking.
182 + const existing = useFiber(current, priority);
183 + existing.pendingProps = coroutine;
184 + existing.return = returnFiber;
185 + return existing;
186 }
187 + }
188
65 - switch (newChildren.$$typeof) {
66 - case REACT_ELEMENT_TYPE: {
67 - const element = (newChildren : ReactElement<any>);
68 - if (existingChild &&
69 - element.type === existingChild.type &&
70 - element.key === existingChild.key) {
71 - // TODO: This is not sufficient since previous siblings could be new.
72 - // Will fix reconciliation properly later.
73 - const clone = shouldClone ? cloneFiber(existingChild, priority) : existingChild;
74 - if (!shouldClone) {
75 - // TODO: This might be lowering the priority of nested unfinished work.
76 - clone.pendingWorkPriority = priority;
189 + function updateYield(
190 + returnFiber : Fiber,
191 + current : ?Fiber,
192 + yieldNode : ReactYield,
193 + priority : PriorityLevel
194 + ) {
195 + // TODO: Should this also compare continuation to determine whether to reuse?
196 + if (current == null || current.tag !== YieldComponent) {
197 + // Insert
198 + const reifiedYield = createReifiedYield(yieldNode);
199 + const created = createFiberFromYield(yieldNode, priority);
200 + created.output = reifiedYield;
201 + created.return = returnFiber;
202 + return created;
203 + } else {
204 + // Move based on index, TODO: This needs to restore a deletion marking.
205 + const existing = useFiber(current, priority);
206 + existing.output = createUpdatedReifiedYield(
207 + current.output,
208 + yieldNode
209 + );
210 + existing.return = returnFiber;
211 + return existing;
212 + }
213 + }
214 +
215 + function updateFragment(
216 + returnFiber : Fiber,
217 + current : ?Fiber,
218 + fragment : Iterable<*>,
219 + priority : PriorityLevel
220 + ) {
221 + if (current == null || current.tag !== Fragment) {
222 + // Insert
223 + const created = createFiberFromFragment(fragment, priority);
224 + created.return = returnFiber;
225 + return created;
226 + } else {
227 + // Update
228 + const existing = useFiber(current, priority);
229 + existing.pendingProps = fragment;
230 + existing.return = returnFiber;
231 + return existing;
232 + }
233 + }
234 +
235 + function updateSlot(
236 + returnFiber : Fiber,
237 + oldFiber : Fiber,
238 + newChild : any,
239 + priority : PriorityLevel
240 + ) : ?Fiber {
241 + // Update the fiber if the keys match, otherwise return null.
242 +
243 + if (typeof newChild === 'string' || typeof newChild === 'number') {
244 + // Text nodes doesn't have keys. If the previous node is implicitly keyed
245 + // we can continue to replace it without aborting even if it is not a text
246 + // node.
247 + if (oldFiber.key !== null) {
248 + return null;
249 + }
250 + return updateTextNode(returnFiber, oldFiber, '' + newChild, priority);
251 + }
252 +
253 + if (typeof newChild === 'object' && newChild !== null) {
254 + switch (newChild.$$typeof) {
255 + case REACT_ELEMENT_TYPE: {
256 + if (newChild.key === oldFiber.key) {
257 + return updateElement(
258 + returnFiber,
259 + oldFiber,
260 + newChild,
261 + priority
262 + );
263 + } else {
264 + return null;
265 }
78 - clone.pendingProps = element.props;
79 - clone.sibling = null;
80 - clone.return = returnFiber;
81 - previousSibling.sibling = clone;
82 - return clone;
266 }
84 - const child = createFiberFromElement(element, priority);
85 - previousSibling.sibling = child;
86 - child.return = returnFiber;
87 - return child;
88 - }
267
90 - case REACT_COROUTINE_TYPE: {
91 - const coroutine = (newChildren : ReactCoroutine);
92 - const child = createFiberFromCoroutine(coroutine, priority);
93 - previousSibling.sibling = child;
94 - child.return = returnFiber;
95 - return child;
268 + case REACT_COROUTINE_TYPE: {
269 + if (newChild.key === oldFiber.key) {
270 + return updateCoroutine(
271 + returnFiber,
272 + oldFiber,
273 + newChild,
274 + priority
275 + );
276 + } else {
277 + return null;
278 + }
279 + }
280 +
281 + case REACT_YIELD_TYPE: {
282 + if (newChild.key === oldFiber.key) {
283 + return updateYield(
284 + returnFiber,
285 + oldFiber,
286 + newChild,
287 + priority
288 + );
289 + } else {
290 + return null;
291 + }
292 + }
293 }
294
98 - case REACT_YIELD_TYPE: {
99 - const yieldNode = (newChildren : ReactYield);
100 - const reifiedYield = createReifiedYield(yieldNode);
101 - const child = createFiberFromYield(yieldNode, priority);
102 - child.output = reifiedYield;
103 - previousSibling.sibling = child;
104 - child.return = returnFiber;
105 - return child;
295 + if (isArray(newChild) || getIteratorFn(newChild)) {
296 + // Fragments doesn't have keys so if the previous key is implicit we can
297 + // update it.
298 + if (oldFiber.key !== null) {
299 + return null;
300 + }
301 + return updateFragment(returnFiber, oldFiber, newChild, priority);
302 }
303 }
304
109 - if (isArray(newChildren)) {
110 - // Nested arrays have a special fiber type.
111 - const fragment = createFiberFromFragment(newChildren, priority);
112 - previousSibling.sibling = fragment;
113 - fragment.return = returnFiber;
114 - return fragment;
115 - } else {
116 - // TODO: Throw for unknown children.
117 - return previousSibling;
305 + return null;
306 + }
307 +
308 + function updateFromMap(
309 + existingChildren : Map<string, Fiber>,
310 + returnFiber : Fiber,
311 + oldFiber : ?Fiber,
312 + newChild : any,
313 + priority : PriorityLevel
314 + ) : ?Fiber {
315 +
316 + // TODO: If this child matches, we need to undo the deletion. However,
317 + // we don't do that for the updateSlot case because nothing was deleted yet.
318 +
319 + if (typeof newChild === 'string' || typeof newChild === 'number') {
320 + // Text nodes doesn't have keys, so we neither have to check the old nor
321 + // new node for the key. If both are text nodes, they match.
322 + return updateTextNode(returnFiber, oldFiber, '' + newChild, priority);
323 }
324 +
325 + if (typeof newChild === 'object' && newChild !== null) {
326 + switch (newChild.$$typeof) {
327 + case REACT_ELEMENT_TYPE: {
328 + if (newChild.key === null) {
329 + // For implicit keys, we'll use the existing fiber in this slot
330 + // but only if it also is an implicit key.
331 + return updateElement(
332 + returnFiber,
333 + oldFiber && oldFiber.key === null ? oldFiber : null,
334 + newChild,
335 + priority
336 + );
337 + } else {
338 + // For explicit keys we look for an existing fiber in the map.
339 + // TODO: We could test oldFiber.key first incase it happens to be
340 + // the same key but it might not be worth it given the likelihood.
341 + const matchedFiber = existingChildren.get(newChild.key);
342 + return updateElement(
343 + returnFiber,
344 + matchedFiber ? matchedFiber : null,
345 + newChild,
346 + priority
347 + );
348 + }
349 + }
350 +
351 + case REACT_COROUTINE_TYPE: {
352 + if (newChild.key === null) {
353 + // For implicit keys, we'll use the existing fiber in this slot
354 + // but only if it also is an implicit key.
355 + return updateCoroutine(
356 + returnFiber,
357 + oldFiber && oldFiber.key === null ? oldFiber : null,
358 + newChild,
359 + priority
360 + );
361 + } else {
362 + // For explicit keys we look for an existing fiber in the map.
363 + // TODO: We could test oldFiber.key first incase it happens to be
364 + // the same key but it might not be worth it given the likelihood.
365 + const matchedFiber = existingChildren.get(newChild.key);
366 + return updateCoroutine(
367 + returnFiber,
368 + matchedFiber ? matchedFiber : null,
369 + newChild,
370 + priority
371 + );
372 + }
373 + }
374 +
375 + case REACT_YIELD_TYPE: {
376 + if (newChild.key === null) {
377 + // For implicit keys, we'll use the existing fiber in this slot
378 + // but only if it also is an implicit key.
379 + return updateYield(
380 + returnFiber,
381 + oldFiber && oldFiber.key === null ? oldFiber : null,
382 + newChild,
383 + priority
384 + );
385 + } else {
386 + // For explicit keys we look for an existing fiber in the map.
387 + // TODO: We could test oldFiber.key first incase it happens to be
388 + // the same key but it might not be worth it given the likelihood.
389 + const matchedFiber = existingChildren.get(newChild.key);
390 + return updateYield(
391 + returnFiber,
392 + matchedFiber ? matchedFiber : null,
393 + newChild,
394 + priority
395 + );
396 + }
397 + }
398 + }
399 +
400 + if (isArray(newChild) || getIteratorFn(newChild)) {
401 + // Fragments doesn't have keys so if the previous is a fragment, we
402 + // update it.
403 + return updateFragment(returnFiber, oldFiber, newChild, priority);
404 + }
405 + }
406 +
407 + return null;
408 }
409
121 - function createFirstChild(returnFiber, existingChild, newChildren : any, priority) {
122 - if (typeof newChildren === 'string') {
123 - const textNode = createFiberFromText(newChildren, priority);
124 - textNode.return = returnFiber;
125 - return textNode;
410 + function reconcileChildrenArray(
411 + returnFiber : Fiber,
412 + currentFirstChild : ?Fiber,
413 + newChildren : Array<*>,
414 + priority : PriorityLevel) {
415 +
416 + // This algorithm can't optimize by searching from boths ends since we
417 + // don't have backpointers on fibers. I'm trying to see how far we can get
418 + // with that model. If it ends up not being worth the tradeoffs, we can
419 + // add it later.
420 +
421 + // Even with a two ended optimization, we'd want to optimize for the case
422 + // where there are few changes and brute force the comparison instead of
423 + // going for the Map. It'd like to explore hitting that path first in
424 + // forward-only mode and only go for the Map once we notice that we need
425 + // lots of look ahead. This doesn't handle reversal as well as two ended
426 + // search but that's unusual. Besides, for the two ended optimization to
427 + // work on Iterables, we'd need to copy the whole set.
428 +
429 + // In this first iteration, we'll just live with hitting the bad case
430 + // (adding everything to a Map) in for every insert/move.
431 +
432 + let resultingFirstChild : ?Fiber = null;
433 + let previousNewFiber : ?Fiber = null;
434 +
435 + let oldFiber = currentFirstChild;
436 + let newIdx = 0;
437 + for (; oldFiber && newIdx < newChildren.length; newIdx++) {
438 + const nextOldFiber = oldFiber.sibling; // In-case we mutate this fiber.
439 + const newFiber = updateSlot(returnFiber, oldFiber, newChildren[newIdx], priority);
440 + if (!newFiber) {
441 + break;
442 + }
443 + if (!previousNewFiber) {
444 + // TODO: Move out of the loop. This only happens for the first run.
445 + resultingFirstChild = newFiber;
446 + } else {
447 + // TODO: Defer siblings if we're not at the right index for this slot.
448 + // I.e. if we had null values before, then we want to defer this
449 + // for each null value. However, we also don't want to call updateSlot
450 + // with the previous one.
451 + previousNewFiber.sibling = newFiber;
452 + }
453 + previousNewFiber = newFiber;
454 + oldFiber = nextOldFiber;
455 }
456
128 - if (typeof newChildren !== 'object' || newChildren === null) {
129 - return null;
457 + if (newIdx === newChildren.length) {
458 + // We've reached the end of the new children. We can delete the rest.
459 + deleteRemainingChildren(returnFiber, oldFiber);
460 + return resultingFirstChild;
461 }
462
132 - switch (newChildren.$$typeof) {
133 - case REACT_ELEMENT_TYPE: {
134 - const element = (newChildren : ReactElement<any>);
135 - if (existingChild &&
136 - element.type === existingChild.type &&
137 - element.key === existingChild.key) {
138 - // Get the clone of the existing fiber.
139 - const clone = shouldClone ? cloneFiber(existingChild, priority) : existingChild;
140 - if (!shouldClone) {
141 - // TODO: This might be lowering the priority of nested unfinished work.
142 - clone.pendingWorkPriority = priority;
143 - }
144 - clone.pendingProps = element.props;
145 - clone.sibling = null;
146 - clone.return = returnFiber;
147 - return clone;
463 + // Mark all children as deleted and add them to a key map for quick lookups.
464 + const existingChildren = mapAndDeleteRemainingChildren(returnFiber, oldFiber);
465 +
466 + // Keep scanning and use the map to restore deleted items as moves.
467 + for (; newIdx < newChildren.length; newIdx++) {
468 + // TODO: Since the mutation of existing fibers can happen at any order
469 + // we might break the link before we're done with it. :(
470 + const nextOldFiber = oldFiber ? oldFiber.sibling : null;
471 + const newFiber = updateFromMap(
472 + existingChildren,
473 + returnFiber,
474 + oldFiber,
475 + newChildren[newIdx],
476 + priority
477 + );
478 + if (newFiber) {
479 + if (!previousNewFiber) {
480 + resultingFirstChild = newFiber;
481 + } else {
482 + previousNewFiber.sibling = newFiber;
483 }
149 - const child = createFiberFromElement(element, priority);
150 - child.return = returnFiber;
151 - return child;
484 + previousNewFiber = newFiber;
485 + }
486 + // We will keep traversing the oldFiber in order, in case the new child
487 + // has a null key that we'll need to match in the same slot.
488 + if (oldFiber) {
489 + oldFiber = nextOldFiber;
490 }
491 + }
492 +
493 + // TODO: Add deletion side-effects to the returnFiber's side-effects.
494 +
495 + return resultingFirstChild;
496 + }
497 +
498 + function reconcileChildrenIterator(
499 + returnFiber : Fiber,
500 + currentFirstChild : ?Fiber,
501 + newChildren : Iterator<*>,
502 + priority : PriorityLevel) {
503 + // TODO: Copy everything from reconcileChildrenArray but use the iterator
504 + // instead.
505 + return null;
506 + }
507 +
508 + function reconcileSingleTextNode(
509 + returnFiber : Fiber,
510 + currentFirstChild : ?Fiber,
511 + textContent : string,
512 + priority : PriorityLevel
513 + ) {
514 + // There's no need to check for keys on text nodes since we don't have a
515 + // way to define them.
516 + if (currentFirstChild && currentFirstChild.tag === HostText) {
517 + // We already have an existing node so let's just update it and delete
518 + // the rest.
519 + deleteRemainingChildren(returnFiber, currentFirstChild.sibling);
520 + const existing = useFiber(currentFirstChild, priority);
521 + existing.pendingProps = textContent;
522 + existing.return = returnFiber;
523 + return existing;
524 + }
525 + // The existing first child is not a text node so we need to create one
526 + // and delete the existing ones.
527 + deleteRemainingChildren(returnFiber, currentFirstChild);
528 + const created = createFiberFromText(textContent, priority);
529 + created.return = returnFiber;
530 + return created;
531 + }
532
154 - case REACT_COROUTINE_TYPE: {
155 - const coroutine = (newChildren : ReactCoroutine);
156 - const child = createFiberFromCoroutine(coroutine, priority);
157 - child.return = returnFiber;
158 - return child;
533 + function reconcileSingleElement(
534 + returnFiber : Fiber,
535 + currentFirstChild : ?Fiber,
536 + element : ReactElement<any>,
537 + priority : PriorityLevel
538 + ) {
539 + const key = element.key;
540 + let child = currentFirstChild;
541 + while (child) {
542 + // TODO: If key === null and child.key === null, then this only applies to
543 + // the first item in the list.
544 + if (child.key === key) {
545 + if (child.type === element.type) {
546 + deleteRemainingChildren(returnFiber, child.sibling);
547 + const existing = useFiber(child, priority);
548 + existing.pendingProps = element.props;
549 + existing.return = returnFiber;
550 + return existing;
551 + } else {
552 + deleteRemainingChildren(returnFiber, child);
553 + break;
554 + }
555 + } else {
556 + deleteChild(returnFiber, child);
557 }
558 + child = child.sibling;
559 + }
560
161 - case REACT_YIELD_TYPE: {
162 - // A yield results in a fragment fiber whose output is the continuation.
163 - // TODO: When there is only a single child, we can optimize this to avoid
164 - // the fragment.
165 - const yieldNode = (newChildren : ReactYield);
166 - const reifiedYield = createReifiedYield(yieldNode);
167 - const child = createFiberFromYield(yieldNode, priority);
168 - child.output = reifiedYield;
169 - child.return = returnFiber;
170 - return child;
561 + const created = createFiberFromElement(element, priority);
562 + created.return = returnFiber;
563 + return created;
564 + }
565 +
566 + function reconcileSingleCoroutine(
567 + returnFiber : Fiber,
568 + currentFirstChild : ?Fiber,
569 + coroutine : ReactCoroutine,
570 + priority : PriorityLevel
571 + ) {
572 + const key = coroutine.key;
573 + let child = currentFirstChild;
574 + while (child) {
575 + // TODO: If key === null and child.key === null, then this only applies to
576 + // the first item in the list.
577 + if (child.key === key) {
578 + if (child.tag === CoroutineComponent) {
579 + deleteRemainingChildren(returnFiber, child.sibling);
580 + const existing = useFiber(child, priority);
581 + existing.pendingProps = coroutine;
582 + existing.return = returnFiber;
583 + return existing;
584 + } else {
585 + deleteRemainingChildren(returnFiber, child);
586 + break;
587 + }
588 + } else {
589 + deleteChild(returnFiber, child);
590 }
591 + child = child.sibling;
592 }
593
174 - if (isArray(newChildren)) {
175 - // Nested arrays have a special fiber type.
176 - const fragment = createFiberFromFragment(newChildren, priority);
177 - fragment.return = returnFiber;
178 - return fragment;
179 - } else {
180 - // TODO: Throw for unknown children.
181 - return null;
594 + const created = createFiberFromCoroutine(coroutine, priority);
595 + created.return = returnFiber;
596 + return created;
597 + }
598 +
599 + function reconcileSingleYield(
600 + returnFiber : Fiber,
601 + currentFirstChild : ?Fiber,
602 + yieldNode : ReactYield,
603 + priority : PriorityLevel
604 + ) {
605 + const key = yieldNode.key;
606 + let child = currentFirstChild;
607 + while (child) {
608 + // TODO: If key === null and child.key === null, then this only applies to
609 + // the first item in the list.
610 + if (child.key === key) {
611 + if (child.tag === YieldComponent) {
612 + deleteRemainingChildren(returnFiber, child.sibling);
613 + const existing = useFiber(child, priority);
614 + existing.output = createUpdatedReifiedYield(
615 + child.output,
616 + yieldNode
617 + );
618 + existing.return = returnFiber;
619 + return existing;
620 + } else {
621 + deleteRemainingChildren(returnFiber, child);
622 + break;
623 + }
624 + } else {
625 + deleteChild(returnFiber, child);
626 + }
627 + child = child.sibling;
628 }
629 +
630 + const reifiedYield = createReifiedYield(yieldNode);
631 + const created = createFiberFromYield(yieldNode, priority);
632 + created.output = reifiedYield;
633 + created.return = returnFiber;
634 + return created;
635 }
636
185 - // TODO: This API won't work because we'll need to transfer the side-effects of
186 - // unmounting children to the returnFiber.
637 + // TODO: This API will tag the children with the side-effect of the
638 + // reconciliation itself. Deletes have to get added to the side-effect list
639 + // of the return fiber right now. Other side-effects will be added as we
640 + // pass through those children.
641 function reconcileChildFibers(
642 returnFiber : Fiber,
643 currentFirstChild : ?Fiber,
190 - newChild : ReactNodeList,
644 + newChild : any,
645 priority : PriorityLevel
646 ) : ?Fiber {
647 // This function is not recursive.
648 // If the top level item is an array, we treat it as a set of children,
649 // not as a fragment. Nested arrays on the other hand will be treated as
650 // fragment nodes. Recursion happens at the normal flow.
197 - if (isArray(newChild)) {
198 - /* $FlowIssue(>=0.31.0) #12747709
199 - *
200 - * `Array.isArray` is matched syntactically for now until predicate
201 - * support is complete.
202 - */
203 - var newChildren : Array<*> = newChild;
204 - var first : ?Fiber = null;
205 - var prev : ?Fiber = null;
206 - var existing : ?Fiber = currentFirstChild;
207 - for (var i = 0; i < newChildren.length; i++) {
208 - var nextExisting = existing && existing.sibling;
209 - if (prev == null) {
210 - prev = createFirstChild(returnFiber, existing, newChildren[i], priority);
211 - first = prev;
212 - } else {
213 - prev = createSubsequentChild(returnFiber, existing, prev, newChildren[i], priority);
214 - }
215 - if (prev && existing) {
216 - // TODO: This is not correct because there could've been more
217 - // than one sibling consumed but I don't want to return a tuple.
218 - existing = nextExisting;
219 - }
651 +
652 + if (typeof newChild === 'string' || typeof newChild === 'number') {
653 + return reconcileSingleTextNode(
654 + returnFiber,
655 + currentFirstChild,
656 + '' + newChild,
657 + priority
658 + );
659 + }
660 +
661 + if (typeof newChild === 'object' && newChild !== null) {
662 + switch (newChild.$$typeof) {
663 + case REACT_ELEMENT_TYPE:
664 + return reconcileSingleElement(
665 + returnFiber,
666 + currentFirstChild,
667 + newChild,
668 + priority
669 + );
670 +
671 + case REACT_COROUTINE_TYPE:
672 + return reconcileSingleCoroutine(
673 + returnFiber,
674 + currentFirstChild,
675 + newChild,
676 + priority
677 + );
678 +
679 + case REACT_YIELD_TYPE:
680 + return reconcileSingleYield(
681 + returnFiber,
682 + currentFirstChild,
683 + newChild,
684 + priority
685 + );
686 + }
687 +
688 + if (isArray(newChild)) {
689 + return reconcileChildrenArray(
690 + returnFiber,
691 + currentFirstChild,
692 + newChild,
693 + priority
694 + );
695 + }
696 +
697 + const iteratorFn = getIteratorFn(newChild);
698 + if (iteratorFn) {
699 + return reconcileChildrenIterator(
700 + returnFiber,
701 + currentFirstChild,
702 + newChild,
703 + priority
704 + );
705 }
221 - return first;
706 }
223 - return createFirstChild(returnFiber, currentFirstChild, newChild, priority);
707 +
708 + // Remaining cases are all treated as empty.
709 + return deleteRemainingChildren(returnFiber, currentFirstChild);
710 }
711
712 return reconcileChildFibers;
713 }
714
229 -exports.reconcileChildFibers = ChildReconciler(true);
715 +exports.reconcileChildFibers = ChildReconciler(true, true);
716
231 -exports.reconcileChildFibersInPlace = ChildReconciler(false);
717 +exports.reconcileChildFibersInPlace = ChildReconciler(false, true);
718
233 -
234 -function cloneSiblings(current : Fiber, workInProgress : Fiber, returnFiber : Fiber) {
235 - workInProgress.return = returnFiber;
236 - while (current.sibling) {
237 - current = current.sibling;
238 - workInProgress = workInProgress.sibling = cloneFiber(
239 - current,
240 - current.pendingWorkPriority
241 - );
242 - workInProgress.return = returnFiber;
243 - }
244 - workInProgress.sibling = null;
245 -}
719 +exports.mountChildFibersInPlace = ChildReconciler(false, false);
720
721 exports.cloneChildFibers = function(current : ?Fiber, workInProgress : Fiber) {
722 if (!workInProgress.child) {
@@ -252,7 +726,7 @@ exports.cloneChildFibers = function(current : ?Fiber, workInProgress : Fiber) {
726 // We use workInProgress.child since that lets Flow know that it can't be
727 // null since we validated that already. However, as the line above suggests
728 // they're actually the same thing.
255 - const currentChild = workInProgress.child;
729 + let currentChild = workInProgress.child;
730 // TODO: This used to reset the pending priority. Not sure if that is needed.
731 // workInProgress.pendingWorkPriority = current.pendingWorkPriority;
732 // TODO: The below priority used to be set to NoWork which would've
@@ -260,9 +734,19 @@ exports.cloneChildFibers = function(current : ?Fiber, workInProgress : Fiber) {
734 // observable when the first sibling has lower priority work remaining
735 // than the next sibling. At that point we should add tests that catches
736 // this.
263 - const newChild = cloneFiber(currentChild, currentChild.pendingWorkPriority);
737 + let newChild = cloneFiber(currentChild, currentChild.pendingWorkPriority);
738 workInProgress.child = newChild;
265 - cloneSiblings(currentChild, newChild, workInProgress);
739 +
740 + newChild.return = workInProgress;
741 + while (currentChild.sibling) {
742 + currentChild = currentChild.sibling;
743 + newChild = newChild.sibling = cloneFiber(
744 + currentChild,
745 + currentChild.pendingWorkPriority
746 + );
747 + newChild.return = workInProgress;
748 + }
749 + newChild.sibling = null;
750 }
751
752 // If there is no alternate, then we don't need to clone the children.
src/renderers/shared/fiber/ReactFiberBeginWork.js
+13 -1
@@ -21,6 +21,7 @@ import type { PriorityLevel } from 'ReactPriorityLevel';
21 import type { UpdateQueue } from 'ReactFiberUpdateQueue';
22
23 var {
24 + mountChildFibersInPlace,
25 reconcileChildFibers,
26 reconcileChildFibersInPlace,
27 cloneChildFibers,
@@ -74,7 +75,18 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>, g
75 // At this point any memoization is no longer valid since we'll have changed
76 // the children.
77 workInProgress.memoizedProps = null;
77 - if (current && current.child === workInProgress.child) {
78 + if (!current) {
79 + // If this is a fresh new component that hasn't been rendered yet, we
80 + // won't update its child set by applying minimal side-effects. Instead,
81 + // we will add them all to the child before it gets rendered. That means
82 + // we can optimize this reconciliation pass by not tracking side-effects.
83 + workInProgress.child = mountChildFibersInPlace(
84 + workInProgress,
85 + workInProgress.child,
86 + nextChildren,
87 + priorityLevel
88 + );
89 + } else if (current.child === workInProgress.child) {
90 // If the current child is the same as the work in progress, it means that
91 // we haven't yet started any work on these children. Therefore, we use
92 // the clone algorithm to create a copy of all the current children.
src/renderers/shared/fiber/ReactReifiedYield.js
+8 -1
@@ -31,8 +31,15 @@ exports.createReifiedYield = function(yieldNode : ReactYield) : ReifiedYield {
31 };
32
33 exports.createUpdatedReifiedYield = function(previousYield : ReifiedYield, yieldNode : ReactYield) : ReifiedYield {
34 + var fiber = previousYield.continuation;
35 + if (fiber.type !== yieldNode.continuation) {
36 + fiber = createFiberFromElementType(
37 + yieldNode.continuation,
38 + yieldNode.key
39 + );
40 + }
41 return {
35 - continuation: previousYield.continuation,
42 + continuation: fiber,
43 props: yieldNode.props,
44 };
45 };