-
Notifications
You must be signed in to change notification settings - Fork 47.6k
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
[Fiber] Compute the Host Diff During Reconciliation #8607
[Fiber] Compute the Host Diff During Reconciliation #8607
Conversation
@@ -127,6 +127,10 @@ var EventPluginHub = { | |||
// live here; needs to be moved to a better place soon | |||
if (typeof inst.tag === 'number') { | |||
const props = inst.memoizedProps; | |||
if (!props) { | |||
// Work in progress. |
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 is part of TODO 2 because if we have no event listeners, then we don't update the fiber pointer. If we don't update the Fiber pointer, then the new one becomes work in progress. If we try to read from work in progress, this might be null. If it completes, it might have event listeners on it that are not yet committed.
Therefore this isn't a great solution.
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.
Seems like maybe this PR comment should be an inline comment so it doesn't get lost or forgotten?
8eff6da
to
7b2e66a
Compare
} else { | ||
(updatePayload = updatePayload || []).push(propKey, '' + nextHtml); | ||
} | ||
} else { |
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 should probably be (from #8652):
if (nextHtml != null) {
if (lastHtml !== nextHtml) {
(updatePayload = updatePayload || []).push(propKey, '' + nextHtml);
}
} else {
// TODO: ...
7b2e66a
to
f22de36
Compare
(I used GH interface for resolving the conflict.) |
I already rebased locally. |
:-( I should've asked. |
Like politics, rebasing is all about rewriting history. In chronological order or not. |
for (styleName in lastStyle) { | ||
if (lastStyle.hasOwnProperty(styleName)) { | ||
if (!styleUpdates) { | ||
styleUpdates = {}; |
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.
Could we pack style updates in the same array with special keys or would that be too complicated?
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.
Interestingly RN did this by packing all fields into one payload object. Caused a bunch of weird artifacts that made diffing harder to optimize.
We should reevaluate the actual algorithms here but for now I just wanted compatibility with CSSPropertyOperations since it is shared with Stack.
} else if (registrationNameModules.hasOwnProperty(propKey)) { | ||
if (nextProp) { | ||
// We eagerly listen to this even though we haven't committed yet. | ||
ensureListeningTo(rootContainerElement, propKey); |
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.
Why do we listen here instead of the commit?
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.
One of the things this fixes is that getRootHostContainer
is no longer needed in the commit phase if we do it here. As we know, getRootHostContainer
is not safe to use during commits since we may not have the right context.
There's also no need to do this in the commit phase so might as well do it during the time sliced parts.
isCustomComponentTag : boolean, | ||
) : void { | ||
// TODO: Handle wasCustomComponentTag | ||
for (var i = 0; i < updatePayload.length; i+=2) { |
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.
Style nit: i += 2
} | ||
} else if (propKey === DANGEROUSLY_SET_INNER_HTML || | ||
propKey === CHILDREN) { | ||
// TODO: Clear innerHTML. This is currently broken in Fiber because we are |
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.
I think @acdlite said this was solved.
} else if (registrationNameModules.hasOwnProperty(propKey)) { | ||
// This is a special case. If any listener updates we need to ensure | ||
// that the "current" fiber pointer gets updated so we need a commit | ||
// to update this element. |
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 depends on precacheFiber
call in ReactDOMFiber.js
, right? It's a bit non-obvious that an empty array here is needed to trigger the path there even though updateProperties
in this file won't do anything. I guess doesn’t matter if we’ll unify them soon.
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.
Also note that there is an open TODO items related to this.
for (propKey in nextProps) { | ||
var nextProp = nextProps[propKey]; | ||
var lastProp = | ||
lastProps != null ? lastProps[propKey] : undefined; |
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.
I don't think lastProps
can be null here. You can probably read directly now that mount and update is separate.
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.
Nice catch. I wish Flow would warn about this.
(updatePayload = updatePayload || []).push(propKey, null); | ||
} | ||
} | ||
for (propKey in nextProps) { |
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.
While unrelated to this PR, I think the most common path is lastProps
and nextProps
with the same prop keys in the same order. I wonder if we could optimize this as a faster case with just one loop, and only do the second loop if we find a mismatch during the first one.
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.
That assumes that you can iterate both interleaved which for...in
doesn't let you do. It would require something like Object.keys
or a generator with for...in
to be really fast. Not too worried about this though. Something we can micro-optimize easily if needed.
} | ||
} | ||
} else { | ||
// Relies on `updateStylesByID` not mutating `styleUpdates`. |
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.
Stray comment.
} | ||
} else { | ||
// Relies on `updateStylesByID` not mutating `styleUpdates`. | ||
if (!styleUpdates) { |
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.
How can styleUpdates
be truthy here? It could only get created if lastStyle
existed and we wanted to delete some styles. But in this case we would get into if (lastProp)
branch instead of this one. Unless I’m missing something.
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.
The code in this branch is also wrong since it'll push a null
. I'll delete.
} | ||
} | ||
if (styleUpdates) { | ||
(updatePayload = updatePayload || []).push(STYLE, styleUpdates); |
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.
Is it still necessary to delay it until the end? Why? This structure makes the above style code a little hard to follow, especially the case where you have to reassign styleUpdates
right after pushing it to the payload so that it doesn't get skipped when it's falsy.
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.
Donno. I just did a line-by-line translation.
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.
If I don't put it here, I need it in all the places where I could create styleUpdates
. Because there may not be a STYLE field in the nextProps but might be one in lastProps or vice versa. I also don't want to end up pushing it twice. I'm not sure that's better.
95689ce
to
43fcc24
Compare
Ok, this I fixed the outstanding TODO by storing the "current" props on the DOM node and in the React Native instance cache. Not too happy about how this all came together but not really sure what we should do about the event system. This is nice because it minimizes work during commit and initial mount but still relies on "diffing" event listeners during updates to figure out if we should update at all. A plausible alternative model would be to traverse the "current" tree while processing the event, or even process events as part of reconciliation which does that traversal anyway. |
workInProgress.updateQueue = (updatePayload : any); | ||
// If the update payload indicates that there is a change or if there | ||
// is a new ref we mark this as an update. | ||
if (updatePayload || current.ref !== workInProgress.ref) { |
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.
Refs shouldn't schedule an Update
effect anymore, since we added the Ref
effect
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.
👍
@@ -37,6 +37,9 @@ src/renderers/dom/shared/__tests__/ReactRenderDocument-test.js | |||
* should throw on full document render w/ no markup | |||
* supports findDOMNode on full-page components | |||
|
|||
src/renderers/dom/shared/wrappers/__tests__/ReactDOMInput-test.js | |||
* should control a value in reentrant events |
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.
I missed why this is failing.
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.
Ah yes. I missed this too. This is probably either a bug or behavior change regarding event timing.
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.
Fixed: #8839
This then gets stored on updateQueue so that the renderer doesn't need to think about how to store this. It then gets passed into commitUpdate during the commit phase. This allows renderers to do the diffing during the time sliced path, allocate a queue for changes and do only the absolute minimal work to apply those changes in the commit phase. If refs update we still schedule and update.
We now take advantage of the new capability to diff properties early. We do this by generating an update payload in the form of an array with each property name and value that we're about to update.
Wrapper components have custom logic that gets applied at the commit phase so we always need to ensure that we schedule an update for them.
No use case yet and I removed it from commitUpdate earlier.
This illustrates the problem that happens if we store a pointer to the Fiber and then choose not to update that pointer when no properties change. That causes an old Fiber to be retained on the DOM node. Then that Fiber can be reused by the pooling mechanism which then will mutate that Fiber with new event handlers, which makes them active before commit.
This represents the current set of event listeners. By not relying on the Fiber, it allows us to avoid doing any effects in the commit phase when nothing changes. This is a bit ugly. Not super happy how this all came together.
43fcc24
to
ef651e8
Compare
…rrentPropsFromNode - After researching usage of getNodeFromInstance we can test getNodeFromInstance dispatching some events and asserting the id of the currentTarget - After checking git blame for getFiberCurrentPropsFromNode and reading through facebook#8607 I found a test that we can simplify to assert behavior of the function by ensuring event handler props are updated from the fiber props. Swapping out the implementation of this function with `return node[internalInstanceKey].memoizedProps` results in a failure.
…11383) * Rewrite ReactDOMComponentTree-test to test behavior using Public API - Part of #11299 - I've tried to identify cases where code within ReactDOMComponentTree is exercised and have updated accordingly but I'm not entirely sure whether I'm on the right track. I thought I'd PR to get feedback from the community. Looking forward to comments. * Prettier and lint changes * Remove testing of internals and add test cases for testing behavior exhibited after use of getInstanceFromNode * [RFC] Update testing approach to verify exhibited behavior dependent upon methods in ReactDOMComponentTree * Remove tests from event handlers and use sync tests * Prettier changes * Rename variables to be more semantic * Prettier updates * Update test following review - Use beforeEach and afterEach to set up and tear down container element for use in each test - Move any functions specific to one test to within test body (improves readability imo) * Add coverage for getNodeFromInstance and implementation of getFiberCurrentPropsFromNode - After researching usage of getNodeFromInstance we can test getNodeFromInstance dispatching some events and asserting the id of the currentTarget - After checking git blame for getFiberCurrentPropsFromNode and reading through #8607 I found a test that we can simplify to assert behavior of the function by ensuring event handler props are updated from the fiber props. Swapping out the implementation of this function with `return node[internalInstanceKey].memoizedProps` results in a failure.
…acebook#11383) * Rewrite ReactDOMComponentTree-test to test behavior using Public API - Part of facebook#11299 - I've tried to identify cases where code within ReactDOMComponentTree is exercised and have updated accordingly but I'm not entirely sure whether I'm on the right track. I thought I'd PR to get feedback from the community. Looking forward to comments. * Prettier and lint changes * Remove testing of internals and add test cases for testing behavior exhibited after use of getInstanceFromNode * [RFC] Update testing approach to verify exhibited behavior dependent upon methods in ReactDOMComponentTree * Remove tests from event handlers and use sync tests * Prettier changes * Rename variables to be more semantic * Prettier updates * Update test following review - Use beforeEach and afterEach to set up and tear down container element for use in each test - Move any functions specific to one test to within test body (improves readability imo) * Add coverage for getNodeFromInstance and implementation of getFiberCurrentPropsFromNode - After researching usage of getNodeFromInstance we can test getNodeFromInstance dispatching some events and asserting the id of the currentTarget - After checking git blame for getFiberCurrentPropsFromNode and reading through facebook#8607 I found a test that we can simplify to assert behavior of the function by ensuring event handler props are updated from the fiber props. Swapping out the implementation of this function with `return node[internalInstanceKey].memoizedProps` results in a failure.
This uses prepareUpdate to compute the diff to apply. If there is no work to do, we don't schedule any updates at all for the commit phase.
Unfortunately wrapper components have custom logic that gets applied in the commit phase so for now we need to always schedule an update for those.
The payload has an array of only the prop key and values that need to update.
This makes the commit phase in the Sierpinski Triangle demo go from 2.5-3ms to <1.5ms. The overall time probably increases but at least now we won't risk dropping frames as we're updating low-pri content.
A neat side-effect of this is that we no longer depend on any "contextual information" like host context or root instance in the commit phase.
TODO 1:
This doesn't correctly apply the diff when theErrr... This is not true. We can't switch between modes. I just added some unused code at some point because I thought we could.is
property switches from custom component mode to built-in component mode. I'll just need to deal with that when deleting values.TODO 2: A more fundamental problem is that we always need to update the pointer to the "current" fiber atm because of how the lazy event listener thing works. I could probably change it to store the props object instead of going through the fiber.TODO 3: Flow infinite loops. (Updating to 0.37.1 doesn't help. #8608)Fixed using workaround.