-
Notifications
You must be signed in to change notification settings - Fork 50.3k
Proof-of-concept: Express render phase in terms of generator functions #18942
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
Conversation
Combines all the sub-phases of the render phase -- begin, complete, and unwind -- into a single generator function. It works by pushing/popping iterator functions onto a stack. This replaces the stack in ReactFiberStack. The cursor objects used by ReactFiberStack are replaced by module level variables that are referenced via closure. For our release builds, what we would do is compile the generator functions to normal, static functions that push/pop to the stack directly, skipping the overhead of the iterator objects. The first one I've ported is Context providers.
|
This pull request is automatically built and testable in CodeSandbox. To see build info of the built libraries, click here or the icon next to each commit SHA. Latest deployment of this branch, based on commit be85aea:
|
The value is only read by the provider component that overrides it, so this can be expressed as a local variable.
|
This is a rough idea of what I had in mind for the generator output. This is a first draft. I'll add more/better examples later. If anyone is interested in working on the compiler part of this, I wouldn't worry too much about getting it running in the actual codebase. There's probably stuff in the runtime part that I haven't fully fleshed out. But if you can get the basics of storing the continuations and variables in the stack, and then reading them back out again, that would be enormously helpful. That's 90% of the work. Input: let threadLocalVariable = 'initial value';
export function* renderSomeComponent(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes,
): RenderStateMachine {
const prevValue = threadLocalVariable;
threadLocalVariable = 'override subtree with new value';
try {
const newChildren = newProps.children;
reconcileChildren(current, workInProgress, newChildren, renderLanes);
yield workInProgress.child;
} finally {
threadLocalVariable = prevValue;
}
}Output: let stack = [];
let threadLocalVariable = 'initial value';
let offset = 0;
// The arguments are passed in from the work loop, so we don't need to store
// them on the stack
function renderSomeComponent_0(current, workInProgress, renderLanes) {
const prevValue = threadLocalVariable;
threadLocalVariable = 'override subtree with new value';
const newChildren = newProps.children;
reconcileChildren(current, workInProgress, newChildren, renderLanes);
const yieldedValue = workInProgress.child;
// First slot is the continuation to use if the children render successfully
// Second slot is if something throws
stack.push(renderSomeComponent_1, renderSomeComponent_2);
offset += 2;
// Remaining slots are used for local variables
stack.push(prevValue);
offset += 1;
return yieldedValue;
}
// "success" continuation
function renderSomeComponent_1(current, workInProgress, renderLanes) {
// Load local variables
const prevValue = stack[offset + 2];
threadLocalVariable = prevValue;
// Pop local variables
array.pop();
return null;
}
// "error" continuation
// Should probably pick a different example since in this case the two branches are the same :D
function renderSomeComponent_2(current, workInProgress, renderLanes) {
// Load local variables
const prevValue = stack[offset + 2];
threadLocalVariable = prevValue;
// Pop local variables
array.pop();
return null;
}And here's a sketch of the runtime we'd use. Replaces the equivalent functions in function beginWork(current: Fiber | null, workInProgress: Fiber, renderLanes: Lanes) {
let next;
switch (tag) {
case SomeComponent:
next = renderSomeComponent_0(current, workInProgress, renderLanes)
break;
default:
throw Error('Hasn\'t been ported to generators yet: ' + tag) ;
}
return next;
}
function completeWork(current: Fiber | null, workInProgress: Fiber, renderLanes: Lanes) {
const continuation = stack[offset];
const next = continuation(current, workInProgress, renderLanes);
if (next === null) {
// Pop continuations off stack
stack.pop();
stack.pop();
offset -= 2;
}
return next;
}
function unwindWork(current: Fiber | null, workInProgress: Fiber, renderLanes: Lanes) {
const continuation = stack[offset + 1];
const next = continuation(current, workInProgress, renderLanes);
if (next === null) {
// Pop continuations off stack
stack.pop();
stack.pop();
offset -= 2;
}
return next;
} |
|
👋 @acdlite |
|
@chirgjn Yeah you can fork the PR branch ( |
|
This is very simple and fast to implement with EffectfulJS, much simpler than rewriting Regenerator. Let me know if interested. But I also have a few comments if you choose anything else. There is no way to handle variables captured from generators and finally continuations ( Using functions as state callbacks is much slower than Effectful JS can optionally move them to top level if needed, with handling closure captured variables properly, but |
|
Out of curiosity, what's the intended goal / benefit of this approach? |
|
I wrote https://github.com/tylerhou/fiber (HN: https://news.ycombinator.com/item?id=29628772) which may be of interest. |
Combines all the sub-phases of the render phase — begin, complete, and unwind — into a single generator function.
It works by pushing/popping generator objects onto a stack. This replaces the stack in ReactFiberStack.
The cursor objects used by ReactFiberStack are replaced by module level variables that are referenced via closure.
For our release builds, what we would do is compile the generator functions to normal, static functions that push/pop to the stack directly, skipping the overhead of the generator objects.
The first one I've ported is Context providers.