Queue discrete events in microtask (#20669)
* Queue discrete events in microtask * Use callback priority to determine cancellation * Add queueMicrotask to react-reconciler README * Fix invatiant conditon for InputDiscrete * Switch invariant null check * Convert invariant to warning * Remove warning from codes.json
Ricky committed
Jan 27, 2021 at 18:24 UTC
e51bd6c1fa2731c4fcd39300144e917aedfc989b
18 files changed
+111
-27
packages/react-dom/src/client/ReactDOMHostConfig.js
+1
-1
@@ -392,7 +392,7 @@ export const queueMicrotask: any =
392
Promise.resolve(null)
393
.then(callback)
394
.catch(handleErrorInNextTick)
395
- : scheduleTimeout;
395
+ : scheduleTimeout; // TODO: Determine the best fallback here.
396
397
function handleErrorInNextTick(error) {
398
setTimeout(() => {
packages/react-dom/src/events/plugins/__tests__/ChangeEventPlugin-test.js
+9
-2
@@ -730,8 +730,15 @@ describe('ChangeEventPlugin', () => {
730
731
// Flush callbacks.
732
// Now the click update has flushed.
733
- expect(Scheduler).toFlushAndYield(['render: ']);
734
- expect(input.value).toBe('');
733
+ if (gate(flags => flags.enableDiscreteEventMicroTasks)) {
734
+ // Flush microtask queue.
735
+ await null;
736
+ expect(Scheduler).toHaveYielded(['render: ']);
737
+ expect(input.value).toBe('');
738
+ } else {
739
+ expect(Scheduler).toFlushAndYield(['render: ']);
740
+ expect(input.value).toBe('');
741
+ }
742
});
743
744
// @gate experimental
packages/react-dom/src/events/plugins/__tests__/SimpleEventPlugin-test.js
+18
-5
@@ -470,11 +470,24 @@ describe('SimpleEventPlugin', function() {
470
'High-pri count: 7, Low-pri count: 0',
471
]);
472
473
- // At the end, both counters should equal the total number of clicks
474
- expect(Scheduler).toFlushAndYield([
475
- 'High-pri count: 8, Low-pri count: 0',
476
- 'High-pri count: 8, Low-pri count: 8',
477
- ]);
473
+ if (gate(flags => flags.enableDiscreteEventMicroTasks)) {
474
+ // Flush the microtask queue
475
+ await null;
476
+
477
+ // At the end, both counters should equal the total number of clicks
478
+ expect(Scheduler).toHaveYielded([
479
+ 'High-pri count: 8, Low-pri count: 0',
480
+
481
+ // TODO: with cancellation, this required another flush?
482
+ 'High-pri count: 8, Low-pri count: 8',
483
+ ]);
484
+ } else {
485
+ // At the end, both counters should equal the total number of clicks
486
+ expect(Scheduler).toFlushAndYield([
487
+ 'High-pri count: 8, Low-pri count: 0',
488
+ 'High-pri count: 8, Low-pri count: 8',
489
+ ]);
490
+ }
491
expect(button.textContent).toEqual('High-pri count: 8, Low-pri count: 8');
492
});
493
});
packages/react-reconciler/README.md
+4
@@ -203,6 +203,10 @@ You can proxy this to `clearTimeout` or its equivalent in your environment.
203
204
This is a property (not a function) that should be set to something that can never be a valid timeout ID. For example, you can set it to `-1`.
205
206
+#### `queueMicrotask(fn)`
207
+
208
+You can proxy this to `queueMicrotask` or its equivalent in your environment.
209
+
210
#### `isPrimaryRenderer`
211
212
This is a property (not a function) that should be set to `true` if your renderer is the main one on the page. For example, if you're writing a renderer for the Terminal, it makes sense to set it to `true`, but if your renderer is used *on top of* React DOM or some other existing renderer, set it to `false`.
packages/react-reconciler/src/ReactFiberWorkLoop.new.js
+33
-9
@@ -92,6 +92,7 @@ import {
92
warnsIfNotActing,
93
afterActiveInstanceBlur,
94
clearContainer,
95
+ queueMicrotask,
96
} from './ReactFiberHostConfig';
97
98
import {
@@ -216,6 +217,7 @@ import {
217
syncNestedUpdateFlag,
218
} from './ReactProfilerTimer.new';
219
220
+import {enableDiscreteEventMicroTasks} from 'shared/ReactFeatureFlags';
221
// DEV stuff
222
import getComponentName from 'shared/getComponentName';
223
import ReactStrictModeWarnings from './ReactStrictModeWarnings.new';
@@ -714,21 +716,34 @@ function ensureRootIsScheduled(root: FiberRoot, currentTime: number) {
716
// Special case: There's nothing to work on.
717
if (existingCallbackNode !== null) {
718
cancelCallback(existingCallbackNode);
717
- root.callbackNode = null;
718
- root.callbackPriority = NoLanePriority;
719
}
720
+ root.callbackNode = null;
721
+ root.callbackPriority = NoLanePriority;
722
return;
723
}
724
725
// Check if there's an existing task. We may be able to reuse it.
724
- if (existingCallbackNode !== null) {
725
- const existingCallbackPriority = root.callbackPriority;
726
- if (existingCallbackPriority === newCallbackPriority) {
727
- // The priority hasn't changed. We can reuse the existing task. Exit.
728
- return;
726
+ const existingCallbackPriority = root.callbackPriority;
727
+ if (existingCallbackPriority === newCallbackPriority) {
728
+ if (__DEV__) {
729
+ // If we're going to re-use an existing task, it needs to exist.
730
+ // Assume that discrete update microtasks are non-cancellable and null.
731
+ // TODO: Temporary until we confirm this warning is not fired.
732
+ if (
733
+ existingCallbackNode == null &&
734
+ existingCallbackPriority !== InputDiscreteLanePriority
735
+ ) {
736
+ console.error(
737
+ 'Expected scheduled callback to exist. This error is likely caused by a bug in React. Please file an issue.',
738
+ );
739
+ }
740
}
730
- // The priority changed. Cancel the existing callback. We'll schedule a new
731
- // one below.
741
+ // The priority hasn't changed. We can reuse the existing task. Exit.
742
+ return;
743
+ }
744
+
745
+ if (existingCallbackNode != null) {
746
+ // Cancel the existing callback. We'll schedule a new one below.
747
cancelCallback(existingCallbackNode);
748
}
749
@@ -737,6 +752,8 @@ function ensureRootIsScheduled(root: FiberRoot, currentTime: number) {
752
if (newCallbackPriority === SyncLanePriority) {
753
// Special case: Sync React callbacks are scheduled on a special
754
// internal queue
755
+
756
+ // TODO: After enableDiscreteEventMicroTasks lands, we can remove the fake node.
757
newCallbackNode = scheduleSyncCallback(
758
performSyncWorkOnRoot.bind(null, root),
759
);
@@ -745,6 +762,12 @@ function ensureRootIsScheduled(root: FiberRoot, currentTime: number) {
762
ImmediateSchedulerPriority,
763
performSyncWorkOnRoot.bind(null, root),
764
);
765
+ } else if (
766
+ enableDiscreteEventMicroTasks &&
767
+ newCallbackPriority === InputDiscreteLanePriority
768
+ ) {
769
+ queueMicrotask(performSyncWorkOnRoot.bind(null, root));
770
+ newCallbackNode = null;
771
} else {
772
const schedulerPriorityLevel = lanePriorityToSchedulerPriority(
773
newCallbackPriority,
@@ -1871,6 +1894,7 @@ function commitRootImpl(root, renderPriorityLevel) {
1894
// commitRoot never returns a continuation; it always finishes synchronously.
1895
// So we can clear these now to allow a new callback to be scheduled.
1896
root.callbackNode = null;
1897
+ root.callbackPriority = NoLanePriority;
1898
1899
// Update the first and last pending times on this root. The new first
1900
// pending time is whatever is left on the root fiber.
packages/react-reconciler/src/ReactFiberWorkLoop.old.js
+33
-9
@@ -34,6 +34,7 @@ import {
34
disableSchedulerTimeoutInWorkLoop,
35
enableDoubleInvokingEffects,
36
skipUnmountedBoundaries,
37
+ enableDiscreteEventMicroTasks,
38
} from 'shared/ReactFeatureFlags';
39
import ReactSharedInternals from 'shared/ReactSharedInternals';
40
import invariant from 'shared/invariant';
@@ -92,6 +93,7 @@ import {
93
warnsIfNotActing,
94
afterActiveInstanceBlur,
95
clearContainer,
96
+ queueMicrotask,
97
} from './ReactFiberHostConfig';
98
99
import {
@@ -696,21 +698,34 @@ function ensureRootIsScheduled(root: FiberRoot, currentTime: number) {
698
// Special case: There's nothing to work on.
699
if (existingCallbackNode !== null) {
700
cancelCallback(existingCallbackNode);
699
- root.callbackNode = null;
700
- root.callbackPriority = NoLanePriority;
701
}
702
+ root.callbackNode = null;
703
+ root.callbackPriority = NoLanePriority;
704
return;
705
}
706
707
// Check if there's an existing task. We may be able to reuse it.
706
- if (existingCallbackNode !== null) {
707
- const existingCallbackPriority = root.callbackPriority;
708
- if (existingCallbackPriority === newCallbackPriority) {
709
- // The priority hasn't changed. We can reuse the existing task. Exit.
710
- return;
708
+ const existingCallbackPriority = root.callbackPriority;
709
+ if (existingCallbackPriority === newCallbackPriority) {
710
+ if (__DEV__) {
711
+ // If we're going to re-use an existing task, it needs to exist.
712
+ // Assume that discrete update microtasks are non-cancellable and null.
713
+ // TODO: Temporary until we confirm this warning is not fired.
714
+ if (
715
+ existingCallbackNode == null &&
716
+ existingCallbackPriority !== InputDiscreteLanePriority
717
+ ) {
718
+ console.error(
719
+ 'Expected scheduled callback to exist. This error is likely caused by a bug in React. Please file an issue.',
720
+ );
721
+ }
722
}
712
- // The priority changed. Cancel the existing callback. We'll schedule a new
713
- // one below.
723
+ // The priority hasn't changed. We can reuse the existing task. Exit.
724
+ return;
725
+ }
726
+
727
+ if (existingCallbackNode != null) {
728
+ // Cancel the existing callback. We'll schedule a new one below.
729
cancelCallback(existingCallbackNode);
730
}
731
@@ -719,6 +734,8 @@ function ensureRootIsScheduled(root: FiberRoot, currentTime: number) {
734
if (newCallbackPriority === SyncLanePriority) {
735
// Special case: Sync React callbacks are scheduled on a special
736
// internal queue
737
+
738
+ // TODO: After enableDiscreteEventMicroTasks lands, we can remove the fake node.
739
newCallbackNode = scheduleSyncCallback(
740
performSyncWorkOnRoot.bind(null, root),
741
);
@@ -727,6 +744,12 @@ function ensureRootIsScheduled(root: FiberRoot, currentTime: number) {
744
ImmediateSchedulerPriority,
745
performSyncWorkOnRoot.bind(null, root),
746
);
747
+ } else if (
748
+ enableDiscreteEventMicroTasks &&
749
+ newCallbackPriority === InputDiscreteLanePriority
750
+ ) {
751
+ queueMicrotask(performSyncWorkOnRoot.bind(null, root));
752
+ newCallbackNode = null;
753
} else {
754
const schedulerPriorityLevel = lanePriorityToSchedulerPriority(
755
newCallbackPriority,
@@ -1851,6 +1874,7 @@ function commitRootImpl(root, renderPriorityLevel) {
1874
// commitRoot never returns a continuation; it always finishes synchronously.
1875
// So we can clear these now to allow a new callback to be scheduled.
1876
root.callbackNode = null;
1877
+ root.callbackPriority = NoLanePriority;
1878
1879
// Update the first and last pending times on this root. The new first
1880
// pending time is whatever is left on the root fiber.
packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.js
+1
@@ -3528,6 +3528,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
3528
});
3529
3530
// @gate enableCache
3531
+ // @gate !enableDiscreteEventMicroTasks
3532
it('regression: empty render at high priority causes update to be dropped', async () => {
3533
// Reproduces a bug where flushDiscreteUpdates starts a new (empty) render
3534
// pass which cancels a scheduled timeout and causes the fallback never to
packages/react-test-renderer/src/ReactTestHostConfig.js
+1
-1
@@ -228,7 +228,7 @@ export const queueMicrotask =
228
Promise.resolve(null)
229
.then(callback)
230
.catch(handleErrorInNextTick)
231
- : scheduleTimeout;
231
+ : scheduleTimeout; // TODO: Determine the best fallback here.
232
233
function handleErrorInNextTick(error) {
234
setTimeout(() => {
packages/shared/ReactFeatureFlags.js
+2
@@ -152,3 +152,5 @@ export const disableSchedulerTimeoutInWorkLoop = false;
152
153
// Experiment to simplify/improve how transitions are scheduled
154
export const enableTransitionEntanglement = false;
155
+
156
+export const enableDiscreteEventMicroTasks = false;
packages/shared/forks/ReactFeatureFlags.native-fb.js
+1
@@ -59,6 +59,7 @@ export const enableUseRefAccessWarning = false;
59
export const enableRecursiveCommitTraversal = false;
60
export const disableSchedulerTimeoutInWorkLoop = false;
61
export const enableTransitionEntanglement = false;
62
+export const enableDiscreteEventMicroTasks = false;
63
64
// Flow magic to verify the exports of this file match the original version.
65
// eslint-disable-next-line no-unused-vars
packages/shared/forks/ReactFeatureFlags.native-oss.js
+1
@@ -58,6 +58,7 @@ export const enableUseRefAccessWarning = false;
58
export const enableRecursiveCommitTraversal = false;
59
export const disableSchedulerTimeoutInWorkLoop = false;
60
export const enableTransitionEntanglement = false;
61
+export const enableDiscreteEventMicroTasks = false;
62
63
// Flow magic to verify the exports of this file match the original version.
64
// eslint-disable-next-line no-unused-vars
packages/shared/forks/ReactFeatureFlags.test-renderer.js
+1
@@ -58,6 +58,7 @@ export const enableUseRefAccessWarning = false;
58
export const enableRecursiveCommitTraversal = false;
59
export const disableSchedulerTimeoutInWorkLoop = false;
60
export const enableTransitionEntanglement = false;
61
+export const enableDiscreteEventMicroTasks = false;
62
63
// Flow magic to verify the exports of this file match the original version.
64
// eslint-disable-next-line no-unused-vars
packages/shared/forks/ReactFeatureFlags.test-renderer.native.js
+1
@@ -58,6 +58,7 @@ export const enableUseRefAccessWarning = false;
58
export const enableRecursiveCommitTraversal = false;
59
export const disableSchedulerTimeoutInWorkLoop = false;
60
export const enableTransitionEntanglement = false;
61
+export const enableDiscreteEventMicroTasks = false;
62
63
// Flow magic to verify the exports of this file match the original version.
64
// eslint-disable-next-line no-unused-vars
packages/shared/forks/ReactFeatureFlags.test-renderer.www.js
+1
@@ -58,6 +58,7 @@ export const enableUseRefAccessWarning = false;
58
export const enableRecursiveCommitTraversal = false;
59
export const disableSchedulerTimeoutInWorkLoop = false;
60
export const enableTransitionEntanglement = false;
61
+export const enableDiscreteEventMicroTasks = false;
62
63
// Flow magic to verify the exports of this file match the original version.
64
// eslint-disable-next-line no-unused-vars
packages/shared/forks/ReactFeatureFlags.testing.js
+1
@@ -58,6 +58,7 @@ export const enableUseRefAccessWarning = false;
58
export const enableRecursiveCommitTraversal = false;
59
export const disableSchedulerTimeoutInWorkLoop = false;
60
export const enableTransitionEntanglement = false;
61
+export const enableDiscreteEventMicroTasks = false;
62
63
// Flow magic to verify the exports of this file match the original version.
64
// eslint-disable-next-line no-unused-vars
packages/shared/forks/ReactFeatureFlags.testing.www.js
+1
@@ -58,6 +58,7 @@ export const enableUseRefAccessWarning = false;
58
export const enableRecursiveCommitTraversal = false;
59
export const disableSchedulerTimeoutInWorkLoop = false;
60
export const enableTransitionEntanglement = false;
61
+export const enableDiscreteEventMicroTasks = false;
62
63
// Flow magic to verify the exports of this file match the original version.
64
// eslint-disable-next-line no-unused-vars
packages/shared/forks/ReactFeatureFlags.www-dynamic.js
+1
@@ -56,3 +56,4 @@ export const enableUseRefAccessWarning = __VARIANT__;
56
export const enableProfilerNestedUpdateScheduledHook = __VARIANT__;
57
export const disableSchedulerTimeoutInWorkLoop = __VARIANT__;
58
export const enableTransitionEntanglement = __VARIANT__;
59
+export const enableDiscreteEventMicroTasks = __VARIANT__;
packages/shared/forks/ReactFeatureFlags.www.js
+1
@@ -32,6 +32,7 @@ export const {
32
disableNativeComponentFrames,
33
disableSchedulerTimeoutInWorkLoop,
34
enableTransitionEntanglement,
35
+ enableDiscreteEventMicroTasks,
36
} = dynamicFeatureFlags;
37
38
// On WWW, __EXPERIMENTAL__ is used for a new modern build.