[Fizz] Prepare Recursive Loop for More Types (#21186)
* Split out into helper functions This is similar to the structure of beginWork in Fiber. * Split the rendering of a node from recursively rendering a node This lets us reuse render node at the root which doesn't spawn new work.
Sebastian Markbåge committed
Apr 7, 2021 at 14:29 UTC
ad6e6ec7bbf7c37c668db57570b296b2eff69d0a
1 file changed
+225
-158
packages/react-server/src/ReactFizzServer.js
+225
-158
@@ -276,7 +276,164 @@ function fatalError(request: Request, error: mixed): void {
276
closeWithError(request.destination, error);
277
}
278
279
-function renderNode(request: Request, task: Task, node: ReactNodeList): void {
279
+function renderSuspenseBoundary(
280
+ request: Request,
281
+ task: Task,
282
+ props: Object,
283
+): void {
284
+ const parentBoundary = task.blockedBoundary;
285
+ const parentSegment = task.blockedSegment;
286
+
287
+ // We need to push an "empty" thing here to identify the parent suspense boundary.
288
+ pushEmpty(parentSegment.chunks, request.responseState, task.assignID);
289
+ task.assignID = null;
290
+ // Each time we enter a suspense boundary, we split out into a new segment for
291
+ // the fallback so that we can later replace that segment with the content.
292
+ // This also lets us split out the main content even if it doesn't suspend,
293
+ // in case it ends up generating a large subtree of content.
294
+ const fallback: ReactNodeList = props.fallback;
295
+ const content: ReactNodeList = props.children;
296
+
297
+ const fallbackAbortSet: Set<Task> = new Set();
298
+ const newBoundary = createSuspenseBoundary(request, fallbackAbortSet);
299
+ const insertionIndex = parentSegment.chunks.length;
300
+ // The children of the boundary segment is actually the fallback.
301
+ const boundarySegment = createPendingSegment(
302
+ request,
303
+ insertionIndex,
304
+ newBoundary,
305
+ parentSegment.formatContext,
306
+ );
307
+ parentSegment.children.push(boundarySegment);
308
+
309
+ // This segment is the actual child content. We can start rendering that immediately.
310
+ const contentRootSegment = createPendingSegment(
311
+ request,
312
+ 0,
313
+ null,
314
+ parentSegment.formatContext,
315
+ );
316
+ // We mark the root segment as having its parent flushed. It's not really flushed but there is
317
+ // no parent segment so there's nothing to wait on.
318
+ contentRootSegment.parentFlushed = true;
319
+
320
+ // Currently this is running synchronously. We could instead schedule this to pingedTasks.
321
+ // I suspect that there might be some efficiency benefits from not creating the suspended task
322
+ // and instead just using the stack if possible.
323
+ // TODO: Call this directly instead of messing with saving and restoring contexts.
324
+
325
+ // We can reuse the current context and task to render the content immediately without
326
+ // context switching. We just need to temporarily switch which boundary and which segment
327
+ // we're writing to. If something suspends, it'll spawn new suspended task with that context.
328
+ task.blockedBoundary = newBoundary;
329
+ task.blockedSegment = contentRootSegment;
330
+ try {
331
+ // We use the safe form because we don't handle suspending here. Only error handling.
332
+ renderNode(request, task, content);
333
+ contentRootSegment.status = COMPLETED;
334
+ newBoundary.completedSegments.push(contentRootSegment);
335
+ if (newBoundary.pendingTasks === 0) {
336
+ // This must have been the last segment we were waiting on. This boundary is now complete.
337
+ // Therefore we won't need the fallback. We early return so that we don't have to create
338
+ // the fallback.
339
+ return;
340
+ }
341
+ } catch (error) {
342
+ contentRootSegment.status = ERRORED;
343
+ reportError(request, error);
344
+ newBoundary.forceClientRender = true;
345
+ // We don't need to decrement any task numbers because we didn't spawn any new task.
346
+ // We don't need to schedule any task because we know the parent has written yet.
347
+ // We do need to fallthrough to create the fallback though.
348
+ } finally {
349
+ task.blockedBoundary = parentBoundary;
350
+ task.blockedSegment = parentSegment;
351
+ }
352
+
353
+ // We create suspended task for the fallback because we don't want to actually task
354
+ // on it yet in case we finish the main content, so we queue for later.
355
+ const suspendedFallbackTask = createTask(
356
+ request,
357
+ fallback,
358
+ parentBoundary,
359
+ boundarySegment,
360
+ fallbackAbortSet,
361
+ newBoundary.id, // This is the ID we want to give this fallback so we can replace it later.
362
+ );
363
+ // TODO: This should be queued at a separate lower priority queue so that we only task
364
+ // on preparing fallbacks if we don't have any more main content to task on.
365
+ request.pingedTasks.push(suspendedFallbackTask);
366
+}
367
+
368
+function renderHostElement(
369
+ request: Request,
370
+ task: Task,
371
+ type: string,
372
+ props: Object,
373
+): void {
374
+ const segment = task.blockedSegment;
375
+ const children = pushStartInstance(
376
+ segment.chunks,
377
+ type,
378
+ props,
379
+ request.responseState,
380
+ segment.formatContext,
381
+ task.assignID,
382
+ );
383
+ // We must have assigned it already above so we don't need this anymore.
384
+ task.assignID = null;
385
+ const prevContext = segment.formatContext;
386
+ segment.formatContext = getChildFormatContext(prevContext, type, props);
387
+ // We use the non-destructive form because if something suspends, we still
388
+ // need to pop back up and finish this subtree of HTML.
389
+ renderNode(request, task, children);
390
+ // We expect that errors will fatal the whole task and that we don't need
391
+ // the correct context. Therefore this is not in a finally.
392
+ segment.formatContext = prevContext;
393
+ pushEndInstance(segment.chunks, type, props);
394
+}
395
+
396
+function renderFunctionComponent(
397
+ request: Request,
398
+ task: Task,
399
+ type: (props: any) => ReactNodeList,
400
+ props: any,
401
+): void {
402
+ const result = type(props);
403
+ // We're now successfully past this task, and we don't have to pop back to
404
+ // the previous task every again, so we can use the destructive recursive form.
405
+ renderNodeDestructive(request, task, result);
406
+}
407
+
408
+function renderElement(
409
+ request: Request,
410
+ task: Task,
411
+ type: any,
412
+ props: Object,
413
+ node: ReactNodeList,
414
+): void {
415
+ if (typeof type === 'function') {
416
+ renderFunctionComponent(request, task, type, props);
417
+ } else if (typeof type === 'string') {
418
+ renderHostElement(request, task, type, props);
419
+ } else if (type === REACT_SUSPENSE_TYPE) {
420
+ renderSuspenseBoundary(request, task, props);
421
+ } else {
422
+ throw new Error('Not yet implemented element type.');
423
+ }
424
+}
425
+
426
+// This function by it self renders a node and consumes the task by mutating it
427
+// to update the current execution state.
428
+function renderNodeDestructive(
429
+ request: Request,
430
+ task: Task,
431
+ node: ReactNodeList,
432
+): void {
433
+ // Stash the node we're working on. We'll pick up from this task in case
434
+ // something suspends.
435
+ task.node = node;
436
+
437
if (typeof node === 'string') {
438
pushTextInstance(
439
task.blockedSegment.chunks,
@@ -291,6 +448,9 @@ function renderNode(request: Request, task: Task, node: ReactNodeList): void {
448
if (isArray(node)) {
449
if (node.length > 0) {
450
for (let i = 0; i < node.length; i++) {
451
+ // Recursively render the rest. We need to use the non-destructive form
452
+ // so that we can safely pop back up and render the sibling if something
453
+ // suspends.
454
renderNode(request, task, node[i]);
455
}
456
} else {
@@ -310,151 +470,71 @@ function renderNode(request: Request, task: Task, node: ReactNodeList): void {
470
}
471
472
if (
313
- typeof node !== 'object' ||
314
- !node ||
315
- (node: any).$$typeof !== REACT_ELEMENT_TYPE
473
+ typeof node === 'object' &&
474
+ node &&
475
+ (node: any).$$typeof === REACT_ELEMENT_TYPE
476
) {
317
- throw new Error('Not yet implemented node type.');
477
+ const element: React$Element<any> = (node: any);
478
+ const type = element.type;
479
+ const props = element.props;
480
+ renderElement(request, task, type, props, node);
481
+ return;
482
}
319
- const element: React$Element<any> = (node: any);
320
- const type = element.type;
321
- const props = element.props;
322
- if (typeof type === 'function') {
323
- try {
324
- const result = type(props);
325
- renderNode(request, task, result);
326
- } catch (x) {
327
- if (typeof x === 'object' && x !== null && typeof x.then === 'function') {
328
- // Something suspended, we'll need to create a new segment and resolve it later.
329
- const segment = task.blockedSegment;
330
- const insertionIndex = segment.chunks.length;
331
- const newSegment = createPendingSegment(
332
- request,
333
- insertionIndex,
334
- null,
335
- segment.formatContext,
336
- );
337
- segment.children.push(newSegment);
338
- const newTask = createTask(
339
- request,
340
- node,
341
- task.blockedBoundary,
342
- newSegment,
343
- task.abortSet,
344
- task.assignID,
345
- );
346
- // We've delegated the assignment.
347
- task.assignID = null;
348
- const ping = newTask.ping;
349
- x.then(ping, ping);
350
- } else {
351
- // We can rethrow to terminate the rest of this tree.
352
- throw x;
353
- }
354
- }
355
- } else if (typeof type === 'string') {
356
- const segment = task.blockedSegment;
357
- const children = pushStartInstance(
358
- segment.chunks,
359
- type,
360
- props,
361
- request.responseState,
362
- segment.formatContext,
363
- task.assignID,
364
- );
365
- // We must have assigned it already above so we don't need this anymore.
366
- task.assignID = null;
367
- const prevContext = segment.formatContext;
368
- segment.formatContext = getChildFormatContext(prevContext, type, props);
369
- renderNode(request, task, children);
370
- // We expect that errors will fatal the whole task and that we don't need
371
- // the correct context. Therefore this is not in a finally.
372
- segment.formatContext = prevContext;
373
- pushEndInstance(segment.chunks, type, props);
374
- } else if (type === REACT_SUSPENSE_TYPE) {
375
- const parentBoundary = task.blockedBoundary;
376
- const parentSegment = task.blockedSegment;
483
378
- // We need to push an "empty" thing here to identify the parent suspense boundary.
379
- pushEmpty(parentSegment.chunks, request.responseState, task.assignID);
380
- task.assignID = null;
381
- // Each time we enter a suspense boundary, we split out into a new segment for
382
- // the fallback so that we can later replace that segment with the content.
383
- // This also lets us split out the main content even if it doesn't suspend,
384
- // in case it ends up generating a large subtree of content.
385
- const fallback: ReactNodeList = props.fallback;
386
- const content: ReactNodeList = props.children;
387
-
388
- const fallbackAbortSet: Set<Task> = new Set();
389
- const newBoundary = createSuspenseBoundary(request, fallbackAbortSet);
390
- const insertionIndex = parentSegment.chunks.length;
391
- // The children of the boundary segment is actually the fallback.
392
- const boundarySegment = createPendingSegment(
393
- request,
394
- insertionIndex,
395
- newBoundary,
396
- parentSegment.formatContext,
397
- );
398
- parentSegment.children.push(boundarySegment);
399
-
400
- // This segment is the actual child content. We can start rendering that immediately.
401
- const contentRootSegment = createPendingSegment(
402
- request,
403
- 0,
404
- null,
405
- parentSegment.formatContext,
406
- );
407
- // We mark the root segment as having its parent flushed. It's not really flushed but there is
408
- // no parent segment so there's nothing to wait on.
409
- contentRootSegment.parentFlushed = true;
410
-
411
- // Currently this is running synchronously. We could instead schedule this to pingedTasks.
412
- // I suspect that there might be some efficiency benefits from not creating the suspended task
413
- // and instead just using the stack if possible.
414
- // TODO: Call this directly instead of messing with saving and restoring contexts.
415
-
416
- // We can reuse the current context and task to render the content immediately without
417
- // context switching. We just need to temporarily switch which boundary and which segment
418
- // we're writing to. If something suspends, it'll spawn new suspended task with that context.
419
- task.blockedBoundary = newBoundary;
420
- task.blockedSegment = contentRootSegment;
421
- try {
422
- renderNode(request, task, content);
423
- contentRootSegment.status = COMPLETED;
424
- newBoundary.completedSegments.push(contentRootSegment);
425
- if (newBoundary.pendingTasks === 0) {
426
- // This must have been the last segment we were waiting on. This boundary is now complete.
427
- // Therefore we won't need the fallback. We early return so that we don't have to create
428
- // the fallback.
429
- return;
430
- }
431
- } catch (error) {
432
- contentRootSegment.status = ERRORED;
433
- reportError(request, error);
434
- newBoundary.forceClientRender = true;
435
- // We don't need to decrement any task numbers because we didn't spawn any new task.
436
- // We don't need to schedule any task because we know the parent has written yet.
437
- // We do need to fallthrough to create the fallback though.
438
- } finally {
439
- task.blockedBoundary = parentBoundary;
440
- task.blockedSegment = parentSegment;
441
- }
484
+ throw new Error('Not yet implemented node type.');
485
+}
486
443
- // We create suspended task for the fallback because we don't want to actually task
444
- // on it yet in case we finish the main content, so we queue for later.
445
- const suspendedFallbackTask = createTask(
446
- request,
447
- fallback,
448
- parentBoundary,
449
- boundarySegment,
450
- fallbackAbortSet,
451
- newBoundary.id, // This is the ID we want to give this fallback so we can replace it later.
452
- );
453
- // TODO: This should be queued at a separate lower priority queue so that we only task
454
- // on preparing fallbacks if we don't have any more main content to task on.
455
- request.pingedTasks.push(suspendedFallbackTask);
456
- } else {
457
- throw new Error('Not yet implemented element type.');
487
+function spawnNewSuspendedTask(
488
+ request: Request,
489
+ task: Task,
490
+ x: Promise<any>,
491
+): void {
492
+ // Something suspended, we'll need to create a new segment and resolve it later.
493
+ const segment = task.blockedSegment;
494
+ const insertionIndex = segment.chunks.length;
495
+ const newSegment = createPendingSegment(
496
+ request,
497
+ insertionIndex,
498
+ null,
499
+ segment.formatContext,
500
+ );
501
+ segment.children.push(newSegment);
502
+ const newTask = createTask(
503
+ request,
504
+ task.node,
505
+ task.blockedBoundary,
506
+ newSegment,
507
+ task.abortSet,
508
+ task.assignID,
509
+ );
510
+ // We've delegated the assignment.
511
+ task.assignID = null;
512
+ const ping = newTask.ping;
513
+ x.then(ping, ping);
514
+}
515
+
516
+// This is a non-destructive form of rendering a node. If it suspends it spawns
517
+// a new task and restores the context of this task to what it was before.
518
+function renderNode(request: Request, task: Task, node: ReactNodeList): void {
519
+ // TODO: Store segment.children.length here and reset it in case something
520
+ // suspended partially through writing something.
521
+
522
+ // Snapshot the current context in case something throws to interrupt the
523
+ // process.
524
+ const previousContext = task.blockedSegment.formatContext;
525
+ try {
526
+ return renderNodeDestructive(request, task, node);
527
+ } catch (x) {
528
+ if (typeof x === 'object' && x !== null && typeof x.then === 'function') {
529
+ spawnNewSuspendedTask(request, task, x);
530
+ // Restore the context. We assume that this will be restored by the inner
531
+ // functions in case nothing throws so we don't use "finally" here.
532
+ task.blockedSegment.formatContext = previousContext;
533
+ } else {
534
+ // We assume that we don't need the correct context.
535
+ // Let's terminate the rest of the tree and don't render any siblings.
536
+ throw x;
537
+ }
538
}
539
}
540
@@ -610,22 +690,9 @@ function retryTask(request: Request, task: Task): void {
690
return;
691
}
692
try {
613
- let node = task.node;
614
- while (
615
- typeof node === 'object' &&
616
- node !== null &&
617
- (node: any).$$typeof === REACT_ELEMENT_TYPE &&
618
- typeof node.type === 'function'
619
- ) {
620
- // Doing this here lets us reuse this same Segment if the next component
621
- // also suspends.
622
- const element: React$Element<any> = (node: any);
623
- task.node = node;
624
- // TODO: Classes and legacy context etc.
625
- node = element.type(element.props);
626
- }
627
-
628
- renderNode(request, task, node);
693
+ // We call the destructive form that mutates this task. That way if something
694
+ // suspends again, we can reuse the same task instead of spawning a new one.
695
+ renderNodeDestructive(request, task, task.node);
696
697
task.abortSet.delete(task);
698
segment.status = COMPLETED;