Skip to content

Commit e3debd7

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 f10215d commit e3debd7

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
@@ -320,43 +320,22 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
320320
callback,
321321
);
322322
}
323-
const isTopLevelUnmount = nextState.element === null;
324323
const update = {
325324
priorityLevel,
326325
expirationTime,
327326
partialState: nextState,
328327
callback,
329328
isReplace: false,
330329
isForced: false,
331-
isTopLevelUnmount,
330+
nextCallback: null,
332331
next: null,
333332
};
334-
const update2 = insertUpdateIntoFiber(current, update, currentTime);
335-
336-
if (isTopLevelUnmount) {
337-
// TODO: Redesign the top-level mount/update/unmount API to avoid this
338-
// special case.
339-
const queue1 = current.updateQueue;
340-
const queue2 = current.alternate !== null
341-
? current.alternate.updateQueue
342-
: null;
343-
344-
// Drop all updates that are lower-priority, so that the tree is not
345-
// remounted. We need to do this for both queues.
346-
if (queue1 !== null && update.next !== null) {
347-
update.next = null;
348-
queue1.last = update;
349-
}
350-
if (queue2 !== null && update2 !== null && update2.next !== null) {
351-
update2.next = null;
352-
queue2.last = update;
353-
}
354-
}
333+
insertUpdateIntoFiber(current, update, currentTime);
355334

356335
if (isPrerender) {
357336
// Block the root from committing at this expiration time.
358337
if (root.topLevelBlockers === null) {
359-
root.topLevelBlockers = createUpdateQueue();
338+
root.topLevelBlockers = createUpdateQueue(null);
360339
}
361340
const block = {
362341
priorityLevel: null,
@@ -365,7 +344,7 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
365344
callback: null,
366345
isReplace: false,
367346
isForced: false,
368-
isTopLevelUnmount: false,
347+
nextCallback: null,
369348
next: null,
370349
};
371350
insertUpdateIntoQueue(root.topLevelBlockers, block, currentTime);
@@ -386,7 +365,7 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
386365
if (topLevelBlockers === null) {
387366
return;
388367
}
389-
processUpdateQueue(topLevelBlockers, null, null, null, expirationTime);
368+
processUpdateQueue(topLevelBlockers, null, null, expirationTime);
390369
expireWork(root, expirationTime);
391370
};
392371
WorkNode.prototype.then = function(callback) {
@@ -437,7 +416,7 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
437416

438417
let completionCallbacks = container.completionCallbacks;
439418
if (completionCallbacks === null) {
440-
completionCallbacks = createUpdateQueue();
419+
completionCallbacks = createUpdateQueue(null);
441420
}
442421

443422
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
@@ -385,21 +385,23 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
385385
// the end of the current batch.
386386
const completionCallbacks = root.completionCallbacks;
387387
if (completionCallbacks !== null) {
388-
processUpdateQueue(completionCallbacks, null, null, null, completedAt);
389-
const callbackList = completionCallbacks.callbackList;
390-
if (callbackList !== null) {
391-
// Add new callbacks to list of completion callbacks
388+
processUpdateQueue(completionCallbacks, null, null, completedAt);
389+
// Add new callbacks to list of completion callbacks
390+
let callbackNode = completionCallbacks.firstCallback;
391+
completionCallbacks.firstCallback = completionCallbacks.lastCallback = null;
392+
while (callbackNode !== null) {
393+
const callback: () => mixed = (callbackNode.callback: any);
394+
// Remove this callback from the update object in case it's still part
395+
// of the queue, so that we don't call it again.
396+
callbackNode.callback = null;
392397
if (rootCompletionCallbackList === null) {
393-
rootCompletionCallbackList = callbackList;
398+
rootCompletionCallbackList = [callback];
394399
} else {
395-
for (let i = 0; i < callbackList.length; i++) {
396-
rootCompletionCallbackList.push(callbackList[i]);
397-
}
398-
}
399-
completionCallbacks.callbackList = null;
400-
if (completionCallbacks.first === null) {
401-
root.completionCallbacks = null;
400+
rootCompletionCallbackList.push(callback);
402401
}
402+
const nextCallback = callbackNode.nextCallback;
403+
callbackNode.nextCallback = null;
404+
callbackNode = nextCallback;
403405
}
404406
}
405407
}
@@ -1635,12 +1637,12 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
16351637
callback,
16361638
isReplace: false,
16371639
isForced: false,
1638-
isTopLevelUnmount: false,
1640+
nextCallback: null,
16391641
next: null,
16401642
};
16411643
const currentTime = recalculateCurrentTime();
16421644
if (root.completionCallbacks === null) {
1643-
root.completionCallbacks = createUpdateQueue();
1645+
root.completionCallbacks = createUpdateQueue(null);
16441646
}
16451647
insertUpdateIntoQueue(root.completionCallbacks, update, currentTime);
16461648
if (expirationTime === root.completedAt) {

0 commit comments

Comments
 (0)