Skip to content

Commit 7887ceb

Browse files
committed
Completion callbacks resolve synchronously if tree is already complete
More unit tests. These completion callbacks (as I'm calling them) have some interesting properties.
1 parent 3bc04f6 commit 7887ceb

File tree

4 files changed

+180
-26
lines changed

4 files changed

+180
-26
lines changed

src/renderers/noop/ReactNoopEntry.js

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ var ReactFiberInstrumentation = require('ReactFiberInstrumentation');
2626
var ReactFiberReconciler = require('ReactFiberReconciler');
2727
var ReactInstanceMap = require('ReactInstanceMap');
2828
var emptyObject = require('fbjs/lib/emptyObject');
29+
var invariant = require('fbjs/lib/invariant');
2930

3031
var expect = require('jest-matchers');
3132

@@ -285,6 +286,27 @@ var ReactNoop = {
285286
}
286287
},
287288

289+
create(rootID: string) {
290+
rootID = typeof rootID === 'string' ? rootID : DEFAULT_ROOT_ID;
291+
invariant(
292+
!roots.has(rootID),
293+
'Root with id %s already exists. Choose a different id.',
294+
rootID,
295+
);
296+
const container = {rootID: rootID, children: []};
297+
rootContainers.set(rootID, container);
298+
const root = NoopRenderer.createContainer(container);
299+
roots.set(rootID, root);
300+
return {
301+
prerender(children: any) {
302+
return NoopRenderer.updateRoot(children, root, null);
303+
},
304+
getChildren() {
305+
return ReactNoop.getChildren(rootID);
306+
},
307+
};
308+
},
309+
288310
findInstance(
289311
componentOrElement: Element | ?React$Component<any, any>,
290312
): null | Instance | TextInstance {

src/renderers/shared/fiber/ReactFiberReconciler.js

Lines changed: 4 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414

1515
import type {Fiber} from 'ReactFiber';
1616
import type {FiberRoot} from 'ReactFiberRoot';
17-
import type {PriorityLevel} from 'ReactPriorityLevel';
1817
import type {ExpirationTime} from 'ReactFiberExpirationTime';
1918
import type {ReactNodeList} from 'ReactTypes';
2019

@@ -218,6 +217,7 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
218217

219218
var {
220219
scheduleUpdate,
220+
scheduleCompletionCallback,
221221
getPriorityContext,
222222
getExpirationTimeForPriority,
223223
recalculateCurrentTime,
@@ -315,7 +315,7 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
315315
if (root.blockers === null) {
316316
root.blockers = createUpdateQueue();
317317
}
318-
const blockUpdate = {
318+
const block = {
319319
priorityLevel: null,
320320
expirationTime,
321321
partialState: null,
@@ -325,7 +325,7 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
325325
isTopLevelUnmount: false,
326326
next: null,
327327
};
328-
insertUpdateIntoQueue(root.blockers, blockUpdate, currentTime);
328+
insertUpdateIntoQueue(root.blockers, block, currentTime);
329329
}
330330

331331
scheduleUpdate(current, expirationTime);
@@ -349,25 +349,7 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
349349
WorkNode.prototype.then = function(callback) {
350350
const root = this._reactRootContainer;
351351
const expirationTime = this._expirationTime;
352-
353-
// Add callback to queue of callbacks on the root. It will be called once
354-
// the root completes at the corresponding expiration time.
355-
const update = {
356-
priorityLevel: null,
357-
expirationTime,
358-
partialState: null,
359-
callback,
360-
isReplace: false,
361-
isForced: false,
362-
isTopLevelUnmount: false,
363-
next: null,
364-
};
365-
const currentTime = recalculateCurrentTime();
366-
if (root.completionCallbacks === null) {
367-
root.completionCallbacks = createUpdateQueue();
368-
}
369-
insertUpdateIntoQueue(root.completionCallbacks, update, currentTime);
370-
scheduleUpdate(root.current, expirationTime);
352+
scheduleCompletionCallback(root, callback, expirationTime);
371353
};
372354

373355
return {

src/renderers/shared/fiber/ReactFiberScheduler.js

Lines changed: 53 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,8 @@ var {
9898
var {
9999
getUpdateExpirationTime,
100100
processUpdateQueue,
101+
createUpdateQueue,
102+
insertUpdateIntoQueue,
101103
} = require('ReactFiberUpdateQueue');
102104

103105
var {resetContext} = require('ReactFiberContext');
@@ -369,13 +371,28 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
369371

370372
if (completedAt !== Done) {
371373
// The root completed but was blocked from committing.
372-
373374
if (expirationTime < completedAt) {
374-
// We have work that expires earlier than the completed root. Regardless
375-
// of whether the root is blocked, we should work on it.
375+
// We have work that expires earlier than the completed root.
376376
return expirationTime;
377377
}
378378

379+
// If the expiration time of the pending work is equal to the time at
380+
// which we completed the work-in-progress, it's possible additional
381+
// work was scheduled that happens to fall within the same expiration
382+
// bucket. We need to check the work-in-progress fiber.
383+
if (expirationTime === completedAt) {
384+
const workInProgress = root.current.alternate;
385+
if (
386+
workInProgress !== null &&
387+
(workInProgress.expirationTime !== Done &&
388+
workInProgress.expirationTime <= expirationTime)
389+
) {
390+
// We have more work. Restart the completed tree.
391+
root.completedAt = Done;
392+
return expirationTime;
393+
}
394+
}
395+
379396
// There have been no higher priority updates since we completed the root.
380397
// If it's still blocked, return Done, as if it has no more work. If it's
381398
// no longer blocked, return the time at which it completed so that we
@@ -797,7 +814,7 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
797814
if (isRootBlocked(root, nextRenderExpirationTime)) {
798815
// The root is blocked from committing. Mark it as complete so we
799816
// know we can commit it later without starting new work.
800-
root.completedAt = workInProgress.expirationTime = nextRenderExpirationTime;
817+
root.completedAt = nextRenderExpirationTime;
801818
} else {
802819
// The root is not blocked, so we can commit it now.
803820
pendingCommit = workInProgress;
@@ -1612,6 +1629,37 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
16121629
}
16131630
}
16141631

1632+
function scheduleCompletionCallback(
1633+
root: FiberRoot,
1634+
callback: () => mixed,
1635+
expirationTime: ExpirationTime,
1636+
) {
1637+
// Add callback to queue of callbacks on the root. It will be called once
1638+
// the root completes at the corresponding expiration time.
1639+
const update = {
1640+
priorityLevel: null,
1641+
expirationTime,
1642+
partialState: null,
1643+
callback,
1644+
isReplace: false,
1645+
isForced: false,
1646+
isTopLevelUnmount: false,
1647+
next: null,
1648+
};
1649+
const currentTime = recalculateCurrentTime();
1650+
if (root.completionCallbacks === null) {
1651+
root.completionCallbacks = createUpdateQueue();
1652+
}
1653+
insertUpdateIntoQueue(root.completionCallbacks, update, currentTime);
1654+
if (expirationTime === root.completedAt) {
1655+
// The tree already completed at this expiration time. Resolve the
1656+
// callback synchronously.
1657+
performWork(TaskPriority, null);
1658+
} else {
1659+
scheduleUpdate(root.current, expirationTime);
1660+
}
1661+
}
1662+
16151663
function getPriorityContext(
16161664
fiber: Fiber,
16171665
forceAsync: boolean,
@@ -1751,6 +1799,7 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
17511799

17521800
return {
17531801
scheduleUpdate: scheduleUpdate,
1802+
scheduleCompletionCallback: scheduleCompletionCallback,
17541803
getPriorityContext: getPriorityContext,
17551804
recalculateCurrentTime: recalculateCurrentTime,
17561805
getExpirationTimeForPriority: getExpirationTimeForPriority,
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
/**
2+
* Copyright 2013-present, Facebook, Inc.
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the BSD-style license found in the
6+
* LICENSE file in the root directory of this source tree. An additional grant
7+
* of patent rights can be found in the PATENTS file in the same directory.
8+
*
9+
* @emails react-core
10+
*/
11+
12+
'use strict';
13+
14+
var React;
15+
var ReactNoop;
16+
17+
describe('ReactIncrementalRoot', () => {
18+
beforeEach(() => {
19+
jest.resetModules();
20+
React = require('react');
21+
ReactNoop = require('react-noop-renderer');
22+
});
23+
24+
function span(prop) {
25+
return {type: 'span', children: [], prop};
26+
}
27+
28+
it('prerenders roots', () => {
29+
const root = ReactNoop.create();
30+
const work = root.prerender(<span prop="A" />);
31+
expect(root.getChildren()).toEqual([]);
32+
work.commit();
33+
expect(root.getChildren()).toEqual([span('A')]);
34+
});
35+
36+
it('resolves `then` callback synchronously if tree is already completed', () => {
37+
const root = ReactNoop.create();
38+
const work = root.prerender(<span prop="A" />);
39+
ReactNoop.flush();
40+
let wasCalled = false;
41+
work.then(() => {
42+
wasCalled = true;
43+
});
44+
expect(wasCalled).toBe(true);
45+
});
46+
47+
it('does not restart a completed tree if there were no additional updates', () => {
48+
let ops = [];
49+
function Foo(props) {
50+
ops.push('Foo');
51+
return <span prop={props.children} />;
52+
}
53+
const root = ReactNoop.create();
54+
const work = root.prerender(<Foo>Hi</Foo>);
55+
56+
ReactNoop.flush();
57+
expect(ops).toEqual(['Foo']);
58+
expect(root.getChildren([]));
59+
60+
work.then(() => {
61+
ops.push('Root completed');
62+
work.commit();
63+
ops.push('Root committed');
64+
});
65+
66+
expect(ops).toEqual([
67+
'Foo',
68+
'Root completed',
69+
// Should not re-render Foo
70+
'Root committed',
71+
]);
72+
expect(root.getChildren([span('Hi')]));
73+
});
74+
75+
it('works on a blocked tree if the expiration time is less than or equal to the blocked update', () => {
76+
let ops = [];
77+
function Foo(props) {
78+
ops.push('Foo: ' + props.children);
79+
return <span prop={props.children} />;
80+
}
81+
const root = ReactNoop.create();
82+
root.prerender(<Foo>A</Foo>);
83+
ReactNoop.flush();
84+
85+
expect(ops).toEqual(['Foo: A']);
86+
expect(root.getChildren([]));
87+
88+
// workA and workB have the same expiration time
89+
root.prerender(<Foo>B</Foo>);
90+
ReactNoop.flush();
91+
92+
// Should have re-rendered the root, even though it's blocked
93+
// from committing.
94+
expect(ops).toEqual(['Foo: A', 'Foo: B']);
95+
expect(root.getChildren([]));
96+
});
97+
98+
it(
99+
'does not work on on a blocked tree if the expiration time is greater than the blocked update',
100+
);
101+
});

0 commit comments

Comments
 (0)