-
Notifications
You must be signed in to change notification settings - Fork 48.8k
API for prerendering a top-level update and deferring the commit #11346
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
f1b79b5
7e86ff1
a83c9d4
dadb5fd
8938cdb
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -10,6 +10,17 @@ | |
'use strict'; | ||
|
||
import type {ReactNodeList} from 'shared/ReactTypes'; | ||
// TODO: This type is shared between the reconciler and ReactDOM, but will | ||
// eventually be lifted out to the renderer. | ||
import type { | ||
FiberRoot, | ||
WorkNode as FiberRootWorkNode, | ||
} from 'react-reconciler/src/ReactFiberRoot'; | ||
// TODO: ExpirationTime (or whatever we end up calling it) should be a public | ||
// type that renderers can consume. | ||
import type { | ||
ExpirationTime, | ||
} from 'react-reconciler/src/ReactFiberExpirationTime'; | ||
|
||
require('../shared/checkReact'); | ||
|
||
|
@@ -759,9 +770,90 @@ function createPortal( | |
return ReactPortal.createPortal(children, container, null, key); | ||
} | ||
|
||
type WorkNode = FiberRootWorkNode & { | ||
then(onComplete: () => mixed): void, | ||
commit(): void, | ||
|
||
_reactRootContainer: FiberRoot, | ||
|
||
_completionCallbacks: Array<() => mixed> | null, | ||
_didComplete: boolean, | ||
}; | ||
|
||
function insertWork(root: FiberRoot, work: FiberRootWorkNode) { | ||
// Insert into root's list of work nodes. | ||
const expirationTime = work._expirationTime; | ||
const firstNode = root.firstTopLevelWork; | ||
if (firstNode === null) { | ||
root.firstTopLevelWork = work; | ||
work._next = null; | ||
} else { | ||
// Insert sorted by expiration time then insertion order | ||
let insertAfter = null; | ||
let insertBefore = firstNode; | ||
while ( | ||
insertBefore !== null && | ||
insertBefore._expirationTime <= expirationTime | ||
) { | ||
insertAfter = insertBefore; | ||
insertBefore = insertBefore._next; | ||
} | ||
work._next = insertBefore; | ||
if (insertAfter !== null) { | ||
insertAfter._next = work; | ||
} | ||
} | ||
} | ||
|
||
function Work(root: FiberRoot, defer: boolean, expirationTime: ExpirationTime) { | ||
this._reactRootContainer = root; | ||
|
||
this._defer = defer; | ||
this._expirationTime = expirationTime; | ||
this._next = null; | ||
|
||
this._completionCallbacks = null; | ||
this._didComplete = false; | ||
} | ||
Work.prototype._onComplete = function() { | ||
this._didComplete = true; | ||
// Set this to null so it isn't called again. | ||
this._onComplete = null; | ||
const callbacks = this._completionCallbacks; | ||
if (callbacks === null) { | ||
return; | ||
} | ||
for (let i = 0; i < callbacks.length; i++) { | ||
// TODO: Error handling | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. TODO for when? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For whenever we figure out how errors inside a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. My current thinking is that What I'm not sure about is the rethrow behavior. If there's an unhandled render error, and the |
||
const callback = callbacks[i]; | ||
callback(); | ||
} | ||
}; | ||
Work.prototype.then = function(cb: () => mixed) { | ||
if (this._didComplete) { | ||
cb(); | ||
return; | ||
} | ||
let completionCallbacks = this._completionCallbacks; | ||
if (completionCallbacks === null) { | ||
completionCallbacks = this._completionCallbacks = []; | ||
} | ||
completionCallbacks.push(cb); | ||
|
||
const root = this._reactRootContainer; | ||
const expirationTime = this._expirationTime; | ||
DOMRenderer.requestWork(root, expirationTime); | ||
}; | ||
Work.prototype.commit = function() { | ||
this._defer = false; | ||
const root = this._reactRootContainer; | ||
const expirationTime = this._expirationTime; | ||
DOMRenderer.flushRoot(root, expirationTime); | ||
}; | ||
|
||
type ReactRootNode = { | ||
render(children: ReactNodeList, callback: ?() => mixed): void, | ||
unmount(callback: ?() => mixed): void, | ||
render(children: ReactNodeList, callback: ?() => mixed): WorkNode, | ||
unmount(callback: ?() => mixed): WorkNode, | ||
|
||
_reactRootContainer: *, | ||
}; | ||
|
@@ -777,13 +869,51 @@ function ReactRoot(container: Container, hydrate: boolean) { | |
ReactRoot.prototype.render = function( | ||
children: ReactNodeList, | ||
callback: ?() => mixed, | ||
): void { | ||
): WorkNode { | ||
const root = this._reactRootContainer; | ||
// TODO: Wrapping in batchedUpdates is needed to prevent work on the root from | ||
// starting until after the work object is inserted. Remove it once | ||
// root scheduling is lifted into the renderer. | ||
return DOMRenderer.batchedUpdates(() => { | ||
const expirationTime = DOMRenderer.updateContainer( | ||
children, | ||
root, | ||
null, | ||
null, | ||
); | ||
const work = new Work(root, false, expirationTime); | ||
insertWork(root, work); | ||
return work; | ||
}); | ||
}; | ||
ReactRoot.prototype.prerender = function(children: ReactNodeList): WorkNode { | ||
const root = this._reactRootContainer; | ||
DOMRenderer.updateContainer(children, root, null, callback); | ||
// TODO: Wrapping in batchedUpdates is needed to prevent work on the root from | ||
// starting until after the work object is inserted. Remove it once | ||
// root scheduling is lifted into the renderer. | ||
return DOMRenderer.batchedUpdates(() => { | ||
const expirationTime = DOMRenderer.updateContainer( | ||
children, | ||
root, | ||
null, | ||
null, | ||
); | ||
const work = new Work(root, true, expirationTime); | ||
insertWork(root, work); | ||
return work; | ||
}); | ||
}; | ||
ReactRoot.prototype.unmount = function(callback) { | ||
ReactRoot.prototype.unmount = function(): WorkNode { | ||
const root = this._reactRootContainer; | ||
DOMRenderer.updateContainer(null, root, null, callback); | ||
// TODO: Wrapping in batchedUpdates is needed to prevent work on the root from | ||
// starting until after the work object is inserted. Remove it once | ||
// root scheduling is lifted into the renderer. | ||
return DOMRenderer.batchedUpdates(() => { | ||
const expirationTime = DOMRenderer.updateContainer(null, root, null, null); | ||
const work = new Work(root, false, expirationTime); | ||
insertWork(root, work); | ||
return work; | ||
}); | ||
}; | ||
|
||
var ReactDOM = { | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This feels like a superset of the previous test (same things being tested, but with a few more expects). Should we remove the previous?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No I like having the focused cases so that if I’m debugging this later and those fail I can focus on fixing those first, which may have the effect of fixing the complex ones too.