Reuse the correct child and side-effects when reusing partial work

We need to use the *other* child because we reset it to the
current one on the way up.

We also need to reset the first/last side-effects to that of the
children so that we're committing the right thing.
This commit is contained in:
Sebastian Markbage
2016-07-18 20:00:17 -07:00
parent 9fca812139
commit de6069e550
3 changed files with 89 additions and 4 deletions
@@ -148,6 +148,23 @@ module.exports = function<T, P, I, C>(config : HostConfig<T, P, I, C>) {
} while (child = child.sibling);
}
function reuseChildrenEffects(returnFiber : Fiber, firstChild : Fiber) {
let child = firstChild;
do {
// Ensure that the first and last effect of the parent corresponds
// to the children's first and last effect.
if (!returnFiber.firstEffect) {
returnFiber.firstEffect = child.firstEffect;
}
if (child.lastEffect) {
if (returnFiber.lastEffect) {
returnFiber.lastEffect.nextEffect = child.firstEffect;
}
returnFiber.lastEffect = child.lastEffect;
}
} while (child = child.sibling);
}
function beginWork(current : ?Fiber, workInProgress : Fiber) : ?Fiber {
// The current, flushed, state of this fiber is the alternate.
// Ideally nothing should rely on this, but relying on it here
@@ -191,7 +208,17 @@ module.exports = function<T, P, I, C>(config : HostConfig<T, P, I, C>) {
const priorityLevel = workInProgress.pendingWorkPriority;
workInProgress.pendingProps = null;
workInProgress.pendingWorkPriority = NoWork;
if (workInProgress.child) {
workInProgress.firstEffect = null;
workInProgress.nextEffect = null;
workInProgress.lastEffect = null;
if (workInProgress.child && workInProgress.child.alternate) {
// On the way up here, we reset the child node to be the current one.
// Therefore we have to reuse the alternate. This is super weird.
workInProgress.child = workInProgress.child.alternate;
// Ensure that the effects of reused work are preserved.
reuseChildrenEffects(workInProgress, workInProgress.child);
// If we bail out but still has work with the current priority in this
// subtree, we need to go find it right now. If we don't, we won't flush
// it until the next tick.
@@ -394,8 +394,6 @@ describe('ReactIncremental', function() {
);
}
// Start rendering an update
// Init
ReactNoop.render(<Foo text="foo" step={0} />);
ReactNoop.flush();
@@ -14,7 +14,7 @@
var React;
var ReactNoop;
describe('ReactIncremental', function() {
describe('ReactIncrementalSideEffects', function() {
beforeEach(function() {
React = require('React');
ReactNoop = require('ReactNoop');
@@ -127,6 +127,66 @@ describe('ReactIncremental', function() {
});
it('can reuse side-effects after being preempted', function() {
function Bar(props) {
return <span prop={props.children} />;
}
var middleContent = (
<div>
<Bar>Hello</Bar>
<Bar>World</Bar>
</div>
);
function Foo(props) {
return (
<div hidden={true}>
{
props.step === 0 ?
<div>
<Bar>Hi</Bar>
<Bar>{props.text}</Bar>
</div>
: middleContent
}
</div>
);
}
// Init
ReactNoop.render(<Foo text="foo" step={0} />);
ReactNoop.flush();
expect(ReactNoop.root.children).toEqual([
div(div(span('Hi'), span('foo'))),
]);
// Make a quick update which will schedule low priority work to
// update the middle content.
ReactNoop.render(<Foo text="bar" step={1} />);
ReactNoop.flushLowPri(30);
// The tree remains unchanged.
expect(ReactNoop.root.children).toEqual([
div(div(span('Hi'), span('foo'))),
]);
// The first Bar has already completed its update but we'll interupt it to
// render some higher priority work. The middle content will bailout so
// it remains untouched which means that it should reuse it next time.
ReactNoop.render(<Foo text="foo" step={1} />);
ReactNoop.flush(30);
// Since we did nothing to the middle subtree during the interuption,
// we should be able to reuse the reconciliation work that we already did
// without restarting. The side-effects should still be replayed.
expect(ReactNoop.root.children).toEqual([
div(div(span('Hello'), span('World'))),
]);
});
it('updates a child even though the old props is empty', function() {
function Foo(props) {