Skip to content

Commit d70d52e

Browse files
committed
Deterministic updates
High priority updates typically require less work to render than low priority ones. It's beneficial to flush those first, in their own batch, before working on more expensive low priority ones. We do this even if a high priority is scheduled after a low priority one. However, we don't want this reordering of updates to affect the terminal state. State should be deterministic: once all work has been flushed, the final state should be the same regardless of how they were scheduled. To get both properties, we store updates on the queue in insertion order instead of priority order (always append). Then, when processing the queue, we skip over updates with insufficient priority. Instead of removing updates from the queue right after processing them, we only remove them if there are no unprocessed updates before it in the list. This means that updates may be processed more than once. As a bonus, the new implementation is simpler and requires less code.
1 parent 75e2a6e commit d70d52e

15 files changed

+343
-322
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@
6060
"glob-stream": "^6.1.0",
6161
"gzip-js": "~0.3.2",
6262
"gzip-size": "^3.0.0",
63+
"jasmine-check": "^1.0.0-rc.0",
6364
"jest": "20.1.0-delta.1",
6465
"jest-config": "20.1.0-delta.1",
6566
"jest-jasmine2": "20.1.0-delta.1",

scripts/jest/test-framework-setup.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,4 +68,6 @@ if (process.env.REACT_CLASS_EQUIVALENCE_TEST) {
6868
return expectation;
6969
};
7070
global.expectDev = expectDev;
71+
72+
require('jasmine-check').install();
7173
}

src/renderers/dom/fiber/__tests__/ReactDOMFiberAsync-test.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -284,8 +284,8 @@ describe('ReactDOMFiberAsync', () => {
284284

285285
// Flush the async updates
286286
jest.runAllTimers();
287-
expect(container.textContent).toEqual('BCAD');
288-
expect(ops).toEqual(['BC', 'BCAD']);
287+
expect(container.textContent).toEqual('ABCD');
288+
expect(ops).toEqual(['BC', 'ABCD']);
289289
});
290290
});
291291
});

src/renderers/noop/ReactNoopEntry.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -296,9 +296,18 @@ var ReactNoop = {
296296
const root = NoopRenderer.createContainer(container);
297297
roots.set(rootID, root);
298298
return {
299+
render(children: any) {
300+
const work = NoopRenderer.updateRoot(children, root, null);
301+
work.then(() => work.commit());
302+
},
299303
prerender(children: any) {
300304
return NoopRenderer.updateRoot(children, root, null);
301305
},
306+
unmount() {
307+
roots.delete(rootID);
308+
const work = NoopRenderer.updateRoot(null, root, null);
309+
work.then(() => work.commit());
310+
},
302311
getChildren() {
303312
return ReactNoop.getChildren(rootID);
304313
},

src/renderers/shared/fiber/ReactFiberBeginWork.js

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -339,7 +339,6 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
339339
workInProgress,
340340
updateQueue,
341341
null,
342-
prevState,
343342
null,
344343
renderExpirationTime,
345344
);
@@ -349,7 +348,7 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
349348
resetHydrationState();
350349
return bailoutOnAlreadyFinishedWork(current, workInProgress);
351350
}
352-
const element = state.element;
351+
const element = state !== null ? state.element : null;
353352
if (
354353
root.hydrate &&
355354
(current === null || current.child === null) &&

src/renderers/shared/fiber/ReactFiberClassComponent.js

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -398,7 +398,7 @@ module.exports = function(
398398
const unmaskedContext = getUnmaskedContext(workInProgress);
399399

400400
instance.props = props;
401-
instance.state = state;
401+
instance.state = workInProgress.memoizedState = state;
402402
instance.refs = emptyObject;
403403
instance.context = getMaskedContext(workInProgress, unmaskedContext);
404404

@@ -422,7 +422,6 @@ module.exports = function(
422422
workInProgress,
423423
updateQueue,
424424
instance,
425-
state,
426425
props,
427426
renderExpirationTime,
428427
);
@@ -589,7 +588,6 @@ module.exports = function(
589588
workInProgress,
590589
workInProgress.updateQueue,
591590
instance,
592-
oldState,
593591
newProps,
594592
renderExpirationTime,
595593
);

src/renderers/shared/fiber/ReactFiberCommitWork.js

Lines changed: 19 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,7 @@ var {
2929
clearCaughtError,
3030
} = require('ReactErrorUtils');
3131

32-
var {
33-
Placement,
34-
Update,
35-
Callback,
36-
ContentReset,
37-
} = require('ReactTypeOfSideEffect');
32+
var {Placement, Update, ContentReset} = require('ReactTypeOfSideEffect');
3833

3934
var invariant = require('fbjs/lib/invariant');
4035

@@ -487,16 +482,26 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
487482
}
488483
}
489484

490-
function commitCallbacks(callbackList, context) {
491-
for (let i = 0; i < callbackList.length; i++) {
492-
const callback = callbackList[i];
485+
function commitCallbacks(updateQueue, context) {
486+
let callbackNode = updateQueue.firstCallback;
487+
// Reset the callback list before calling them in case something throws.
488+
updateQueue.firstCallback = updateQueue.lastCallback = null;
489+
490+
while (callbackNode !== null) {
491+
const callback = callbackNode.callback;
492+
// Remove this callback from the update object in case it's still part
493+
// of the queue, so that we don't call it again.
494+
callbackNode.callback = null;
493495
invariant(
494496
typeof callback === 'function',
495497
'Invalid argument passed as callback. Expected a function. Instead ' +
496498
'received: %s',
497499
callback,
498500
);
499501
callback.call(context);
502+
const nextCallback = callbackNode.nextCallback;
503+
callbackNode.nextCallback = null;
504+
callbackNode = nextCallback;
500505
}
501506
}
502507

@@ -529,31 +534,19 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
529534
}
530535
}
531536
}
532-
if (
533-
finishedWork.effectTag & Callback &&
534-
finishedWork.updateQueue !== null
535-
) {
536-
const updateQueue = finishedWork.updateQueue;
537-
if (updateQueue.callbackList !== null) {
538-
// Set the list to null to make sure they don't get called more than once.
539-
const callbackList = updateQueue.callbackList;
540-
updateQueue.callbackList = null;
541-
commitCallbacks(callbackList, instance);
542-
}
537+
const updateQueue = finishedWork.updateQueue;
538+
if (updateQueue !== null) {
539+
commitCallbacks(updateQueue, instance);
543540
}
544541
return;
545542
}
546543
case HostRoot: {
547544
const updateQueue = finishedWork.updateQueue;
548-
if (updateQueue !== null && updateQueue.callbackList !== null) {
549-
// Set the list to null to make sure they don't get called more
550-
// than once.
551-
const callbackList = updateQueue.callbackList;
552-
updateQueue.callbackList = null;
545+
if (updateQueue !== null) {
553546
const instance = finishedWork.child !== null
554547
? finishedWork.child.stateNode
555548
: null;
556-
commitCallbacks(callbackList, instance);
549+
commitCallbacks(updateQueue, instance);
557550
}
558551
return;
559552
}

src/renderers/shared/fiber/ReactFiberReconciler.js

Lines changed: 6 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -286,43 +286,22 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
286286
callback,
287287
);
288288
}
289-
const isTopLevelUnmount = nextState.element === null;
290289
const update = {
291290
priorityLevel,
292291
expirationTime,
293292
partialState: nextState,
294293
callback,
295294
isReplace: false,
296295
isForced: false,
297-
isTopLevelUnmount,
296+
nextCallback: null,
298297
next: null,
299298
};
300-
const update2 = insertUpdateIntoFiber(current, update, currentTime);
301-
302-
if (isTopLevelUnmount) {
303-
// TODO: Redesign the top-level mount/update/unmount API to avoid this
304-
// special case.
305-
const queue1 = current.updateQueue;
306-
const queue2 = current.alternate !== null
307-
? current.alternate.updateQueue
308-
: null;
309-
310-
// Drop all updates that are lower-priority, so that the tree is not
311-
// remounted. We need to do this for both queues.
312-
if (queue1 !== null && update.next !== null) {
313-
update.next = null;
314-
queue1.last = update;
315-
}
316-
if (queue2 !== null && update2 !== null && update2.next !== null) {
317-
update2.next = null;
318-
queue2.last = update;
319-
}
320-
}
299+
insertUpdateIntoFiber(current, update, currentTime);
321300

322301
if (isPrerender) {
323302
// Block the root from committing at this expiration time.
324303
if (root.topLevelBlockers === null) {
325-
root.topLevelBlockers = createUpdateQueue();
304+
root.topLevelBlockers = createUpdateQueue(null);
326305
}
327306
const block = {
328307
priorityLevel: null,
@@ -331,7 +310,7 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
331310
callback: null,
332311
isReplace: false,
333312
isForced: false,
334-
isTopLevelUnmount: false,
313+
nextCallback: null,
335314
next: null,
336315
};
337316
insertUpdateIntoQueue(root.topLevelBlockers, block, currentTime);
@@ -352,7 +331,7 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
352331
if (topLevelBlockers === null) {
353332
return;
354333
}
355-
processUpdateQueue(topLevelBlockers, null, null, null, expirationTime);
334+
processUpdateQueue(topLevelBlockers, null, null, expirationTime);
356335
expireWork(root, expirationTime);
357336
};
358337
WorkNode.prototype.then = function(callback) {
@@ -403,7 +382,7 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
403382

404383
let completionCallbacks = container.completionCallbacks;
405384
if (completionCallbacks === null) {
406-
completionCallbacks = createUpdateQueue();
385+
completionCallbacks = createUpdateQueue(null);
407386
}
408387

409388
return new WorkNode(container, expirationTime);

src/renderers/shared/fiber/ReactFiberScheduler.js

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -386,21 +386,23 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
386386
// the end of the current batch.
387387
const completionCallbacks = root.completionCallbacks;
388388
if (completionCallbacks !== null) {
389-
processUpdateQueue(completionCallbacks, null, null, null, completedAt);
390-
const callbackList = completionCallbacks.callbackList;
391-
if (callbackList !== null) {
392-
// Add new callbacks to list of completion callbacks
389+
processUpdateQueue(completionCallbacks, null, null, completedAt);
390+
// Add new callbacks to list of completion callbacks
391+
let callbackNode = completionCallbacks.firstCallback;
392+
completionCallbacks.firstCallback = completionCallbacks.lastCallback = null;
393+
while (callbackNode !== null) {
394+
const callback: () => mixed = (callbackNode.callback: any);
395+
// Remove this callback from the update object in case it's still part
396+
// of the queue, so that we don't call it again.
397+
callbackNode.callback = null;
393398
if (rootCompletionCallbackList === null) {
394-
rootCompletionCallbackList = callbackList;
399+
rootCompletionCallbackList = [callback];
395400
} else {
396-
for (let i = 0; i < callbackList.length; i++) {
397-
rootCompletionCallbackList.push(callbackList[i]);
398-
}
399-
}
400-
completionCallbacks.callbackList = null;
401-
if (completionCallbacks.first === null) {
402-
root.completionCallbacks = null;
401+
rootCompletionCallbackList.push(callback);
403402
}
403+
const nextCallback = callbackNode.nextCallback;
404+
callbackNode.nextCallback = null;
405+
callbackNode = nextCallback;
404406
}
405407
}
406408
}
@@ -1636,12 +1638,12 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
16361638
callback,
16371639
isReplace: false,
16381640
isForced: false,
1639-
isTopLevelUnmount: false,
1641+
nextCallback: null,
16401642
next: null,
16411643
};
16421644
const currentTime = recalculateCurrentTime();
16431645
if (root.completionCallbacks === null) {
1644-
root.completionCallbacks = createUpdateQueue();
1646+
root.completionCallbacks = createUpdateQueue(null);
16451647
}
16461648
insertUpdateIntoQueue(root.completionCallbacks, update, currentTime);
16471649
if (expirationTime === root.completedAt) {

0 commit comments

Comments
 (0)