@@ -327,6 +327,7 @@ function renderSuspenseBoundary(
327327 task . blockedBoundary = newBoundary ;
328328 task . blockedSegment = contentRootSegment ;
329329 try {
330+ // We use the safe form because we don't handle suspending here. Only error handling.
330331 renderNode ( request , task , content ) ;
331332 contentRootSegment . status = COMPLETED ;
332333 newBoundary . completedSegments . push ( contentRootSegment ) ;
@@ -382,13 +383,27 @@ function renderHostElement(
382383 task . assignID = null ;
383384 const prevContext = segment . formatContext ;
384385 segment . formatContext = getChildFormatContext ( prevContext , type , props ) ;
386+ // We use the non-destructive form because if something suspends, we still
387+ // need to pop back up and finish this subtree of HTML.
385388 renderNode ( request , task , children ) ;
386389 // We expect that errors will fatal the whole task and that we don't need
387390 // the correct context. Therefore this is not in a finally.
388391 segment . formatContext = prevContext ;
389392 pushEndInstance ( segment . chunks , type , props ) ;
390393}
391394
395+ function renderFunctionComponent (
396+ request : Request ,
397+ task : Task ,
398+ type : ( props : any ) = > ReactNodeList ,
399+ props : any ,
400+ ) : void {
401+ const result = type ( props ) ;
402+ // We're now successfully past this task, and we don't have to pop back to
403+ // the previous task every again, so we can use the destructive recursive form.
404+ renderNodeDestructive ( request , task , result ) ;
405+ }
406+
392407function renderElement (
393408 request : Request ,
394409 task : Task ,
@@ -397,38 +412,7 @@ function renderElement(
397412 node : ReactNodeList ,
398413) : void {
399414 if ( typeof type === 'function' ) {
400- try {
401- const result = type ( props ) ;
402- renderNode ( request , task , result ) ;
403- } catch ( x ) {
404- if ( typeof x === 'object' && x !== null && typeof x . then === 'function' ) {
405- // Something suspended, we'll need to create a new segment and resolve it later.
406- const segment = task . blockedSegment ;
407- const insertionIndex = segment . chunks . length ;
408- const newSegment = createPendingSegment (
409- request ,
410- insertionIndex ,
411- null ,
412- segment . formatContext ,
413- ) ;
414- segment . children . push ( newSegment ) ;
415- const newTask = createTask (
416- request ,
417- node ,
418- task . blockedBoundary ,
419- newSegment ,
420- task . abortSet ,
421- task . assignID ,
422- ) ;
423- // We've delegated the assignment.
424- task . assignID = null ;
425- const ping = newTask . ping ;
426- x . then ( ping , ping ) ;
427- } else {
428- // We can rethrow to terminate the rest of this tree.
429- throw x ;
430- }
431- }
415+ renderFunctionComponent ( request , task , type , props ) ;
432416 } else if ( typeof type === 'string' ) {
433417 renderHostElement ( request , task , type , props ) ;
434418 } else if ( type === REACT_SUSPENSE_TYPE ) {
@@ -438,7 +422,17 @@ function renderElement(
438422 }
439423}
440424
441- function renderNode ( request : Request , task : Task , node : ReactNodeList ) : void {
425+ // This function by it self renders a node and consumes the task by mutating it
426+ // to update the current execution state.
427+ function renderNodeDestructive (
428+ request : Request ,
429+ task : Task ,
430+ node : ReactNodeList ,
431+ ) : void {
432+ // Stash the node we're working on. We'll pick up from this task in case
433+ // something suspends.
434+ task . node = node ;
435+
442436 if ( typeof node === 'string' ) {
443437 pushTextInstance (
444438 task . blockedSegment . chunks ,
@@ -453,6 +447,9 @@ function renderNode(request: Request, task: Task, node: ReactNodeList): void {
453447 if ( Array . isArray ( node ) ) {
454448 if ( node . length > 0 ) {
455449 for ( let i = 0 ; i < node . length ; i ++ ) {
450+ // Recursively render the rest. We need to use the non-destructive form
451+ // so that we can safely pop back up and render the sibling if something
452+ // suspends.
456453 renderNode ( request , task , node [ i ] ) ;
457454 }
458455 } else {
@@ -486,6 +483,60 @@ function renderNode(request: Request, task: Task, node: ReactNodeList): void {
486483 throw new Error ( 'Not yet implemented node type.' ) ;
487484}
488485
486+ function spawnNewSuspendedTask (
487+ request : Request ,
488+ task : Task ,
489+ x : Promise < any > ,
490+ ) : void {
491+ // Something suspended, we'll need to create a new segment and resolve it later.
492+ const segment = task . blockedSegment ;
493+ const insertionIndex = segment . chunks . length ;
494+ const newSegment = createPendingSegment (
495+ request ,
496+ insertionIndex ,
497+ null ,
498+ segment . formatContext ,
499+ ) ;
500+ segment . children . push ( newSegment ) ;
501+ const newTask = createTask (
502+ request ,
503+ task . node ,
504+ task . blockedBoundary ,
505+ newSegment ,
506+ task . abortSet ,
507+ task . assignID ,
508+ ) ;
509+ // We've delegated the assignment.
510+ task . assignID = null ;
511+ const ping = newTask . ping ;
512+ x . then ( ping , ping ) ;
513+ }
514+
515+ // This is a non-destructive form of rendering a node. If it suspends it spawns
516+ // a new task and restores the context of this task to what it was before.
517+ function renderNode ( request : Request , task : Task , node : ReactNodeList ) : void {
518+ // TODO: Store segment.children.length here and reset it in case something
519+ // suspended partially through writing something.
520+
521+ // Snapshot the current context in case something throws to interrupt the
522+ // process.
523+ const previousContext = task . blockedSegment . formatContext ;
524+ try {
525+ return renderNodeDestructive ( request , task , node ) ;
526+ } catch ( x ) {
527+ if ( typeof x === 'object' && x !== null && typeof x . then === 'function' ) {
528+ spawnNewSuspendedTask ( request , task , x ) ;
529+ // Restore the context. We assume that this will be restored by the inner
530+ // functions in case nothing throws so we don't use "finally" here.
531+ task . blockedSegment . formatContext = previousContext ;
532+ } else {
533+ // We assume that we don't need the correct context.
534+ // Let's terminate the rest of the tree and don't render any siblings.
535+ throw x ;
536+ }
537+ }
538+ }
539+
489540function erroredTask (
490541 request : Request ,
491542 boundary : Root | SuspenseBoundary ,
@@ -638,22 +689,9 @@ function retryTask(request: Request, task: Task): void {
638689 return ;
639690 }
640691 try {
641- let node = task . node ;
642- while (
643- typeof node === 'object' &&
644- node !== null &&
645- ( node : any ) . $$typeof === REACT_ELEMENT_TYPE &&
646- typeof node . type === 'function'
647- ) {
648- // Doing this here lets us reuse this same Segment if the next component
649- // also suspends.
650- const element : React$Element < any > = ( node : any ) ;
651- task . node = node ;
652- // TODO: Classes and legacy context etc.
653- node = element . type ( element . props ) ;
654- }
655-
656- renderNode ( request , task , node ) ;
692+ // We call the destructive form that mutates this task. That way if something
693+ // suspends again, we can reuse the same task instead of spawning a new one.
694+ renderNodeDestructive ( request , task , task . node ) ;
657695
658696 task . abortSet . delete ( task ) ;
659697 segment . status = COMPLETED ;
0 commit comments