Make event config an implementation detail of each plugin (#19236)
* Merge two variables with same purpose * Replace dispatchConfig with _reactName on event object
Dan Abramov committed
Jul 2, 2020 at 00:03 UTC
991c3b8193bd6bcc53c23c79eddc659eb41a4c3a
13 files changed
+71
-176
packages/react-dom/src/client/ReactDOMComponent.js
+6
-6
@@ -8,7 +8,7 @@
8
*/
9
10
import {
11
- registrationNames,
11
+ registrationNameDependencies,
12
possibleRegistrationNames,
13
} from '../events/EventPluginRegistry';
14
import {canUseDOM} from 'shared/ExecutionEnvironment';
@@ -133,7 +133,7 @@ if (__DEV__) {
133
validateARIAProperties(type, props);
134
validateInputProperties(type, props);
135
validateUnknownProperties(type, props, {
136
- registrationNames,
136
+ registrationNameDependencies,
137
possibleRegistrationNames,
138
});
139
};
@@ -356,7 +356,7 @@ function setInitialDOMProperties(
356
// We could have excluded it in the property list instead of
357
// adding a special case here, but then it wouldn't be emitted
358
// on server rendering (but we *do* want to emit it in SSR).
359
- } else if (registrationNames.hasOwnProperty(propKey)) {
359
+ } else if (registrationNameDependencies.hasOwnProperty(propKey)) {
360
if (nextProp != null) {
361
if (__DEV__ && typeof nextProp !== 'function') {
362
warnForInvalidEventListener(propKey, nextProp);
@@ -694,7 +694,7 @@ export function diffProperties(
694
// Noop
695
} else if (propKey === AUTOFOCUS) {
696
// Noop. It doesn't work on updates anyway.
697
- } else if (registrationNames.hasOwnProperty(propKey)) {
697
+ } else if (registrationNameDependencies.hasOwnProperty(propKey)) {
698
// This is a special case. If any listener updates we need to ensure
699
// that the "current" fiber pointer gets updated so we need a commit
700
// to update this element.
@@ -781,7 +781,7 @@ export function diffProperties(
781
propKey === SUPPRESS_HYDRATION_WARNING
782
) {
783
// Noop
784
- } else if (registrationNames.hasOwnProperty(propKey)) {
784
+ } else if (registrationNameDependencies.hasOwnProperty(propKey)) {
785
if (nextProp != null) {
786
// We eagerly listen to this even though we haven't committed yet.
787
if (__DEV__ && typeof nextProp !== 'function') {
@@ -978,7 +978,7 @@ export function diffHydratedProperties(
978
updatePayload = [CHILDREN, '' + nextProp];
979
}
980
}
981
- } else if (registrationNames.hasOwnProperty(propKey)) {
981
+ } else if (registrationNameDependencies.hasOwnProperty(propKey)) {
982
if (nextProp != null) {
983
if (__DEV__ && typeof nextProp !== 'function') {
984
warnForInvalidEventListener(propKey, nextProp);
packages/react-dom/src/events/DOMEventProperties.js
+5
-9
@@ -13,10 +13,6 @@ import type {
13
DOMTopLevelEventType,
14
} from '../events/TopLevelEventTypes';
15
import type {EventTypes} from '../events/PluginModuleType';
16
-import type {
17
- DispatchConfig,
18
- CustomDispatchConfig,
19
-} from '../events/ReactSyntheticEventType';
16
17
import * as DOMTopLevelEventTypes from './DOMTopLevelEventTypes';
18
import {
@@ -35,9 +31,9 @@ import {enableCreateEventHandleAPI} from 'shared/ReactFeatureFlags';
31
// update the below line.
32
export const simpleEventPluginEventTypes: EventTypes = {};
33
38
-export const topLevelEventsToDispatchConfig: Map<
34
+export const topLevelEventsToReactNames: Map<
35
TopLevelType,
40
- DispatchConfig | CustomDispatchConfig,
36
+ string | null,
37
> = new Map();
38
39
const eventPriorities = new Map();
@@ -167,8 +163,8 @@ const continuousPairsForSimpleEventPlugin = [
163
* },
164
* ...
165
* };
170
- * topLevelEventsToDispatchConfig = new Map([
171
- * [TOP_ABORT, { sameConfig }],
166
+ * topLevelEventsToReactNames = new Map([
167
+ * [TOP_ABORT, 'onAbort'],
168
* ]);
169
*/
170
@@ -197,7 +193,7 @@ function processSimpleEventPluginPairsByPriority(
193
eventPriority: priority,
194
};
195
eventPriorities.set(topEvent, priority);
200
- topLevelEventsToDispatchConfig.set(topEvent, config);
196
+ topLevelEventsToReactNames.set(topEvent, onEvent);
197
simpleEventPluginEventTypes[event] = config;
198
}
199
}
packages/react-dom/src/events/DOMModernPluginEventSystem.js
+9
-23
@@ -16,10 +16,7 @@ import type {
16
DispatchQueueItemPhase,
17
DispatchQueueItemPhaseEntry,
18
} from './PluginModuleType';
19
-import type {
20
- ReactSyntheticEvent,
21
- CustomDispatchConfig,
22
-} from './ReactSyntheticEventType';
19
+import type {ReactSyntheticEvent} from './ReactSyntheticEventType';
20
import type {
21
ElementListenerMap,
22
ElementListenerMapEntry,
@@ -113,7 +110,7 @@ import {
110
addEventCaptureListenerWithPassiveFlag,
111
} from './EventListener';
112
import {removeTrappedEventListener} from './DeprecatedDOMEventResponderSystem';
116
-import {topLevelEventsToDispatchConfig} from './DOMEventProperties';
113
+import {topLevelEventsToReactNames} from './DOMEventProperties';
114
import * as ModernBeforeInputEventPlugin from './plugins/ModernBeforeInputEventPlugin';
115
import * as ModernChangeEventPlugin from './plugins/ModernChangeEventPlugin';
116
import * as ModernEnterLeaveEventPlugin from './plugins/ModernEnterLeaveEventPlugin';
@@ -223,14 +220,6 @@ if (enableCreateEventHandleAPI) {
220
capturePhaseEvents.add(TOP_AFTER_BLUR);
221
}
222
226
-const emptyDispatchConfigForCustomEvents: CustomDispatchConfig = {
227
- customEvent: true,
228
- phasedRegistrationNames: {
229
- bubbled: null,
230
- captured: null,
231
- },
232
-};
233
-
223
function executeDispatch(
224
event: ReactSyntheticEvent,
225
listener: Function,
@@ -639,11 +628,11 @@ export function accumulateTwoPhaseListeners(
628
event: ReactSyntheticEvent,
629
accumulateEventHandleListeners?: boolean,
630
): void {
642
- const phasedRegistrationNames = event.dispatchConfig.phasedRegistrationNames;
631
+ const bubbled = event._reactName;
632
+ const captured = bubbled !== null ? bubbled + 'Capture' : null;
633
const capturePhase: DispatchQueueItemPhase = [];
634
const bubblePhase: DispatchQueueItemPhase = [];
635
646
- const {bubbled, captured} = phasedRegistrationNames;
636
// If we are not handling EventTarget only phase, then we're doing the
637
// usual two phase accumulation using the React fiber tree to pick up
638
// all relevant useEvent and on* prop events.
@@ -826,7 +815,7 @@ function accumulateEnterLeaveListenersForEvent(
815
common: Fiber | null,
816
capture: boolean,
817
): void {
829
- const registrationName = event.dispatchConfig.registrationName;
818
+ const registrationName = event._reactName;
819
if (registrationName === undefined) {
820
return;
821
}
@@ -944,17 +933,14 @@ export function accumulateEventTargetListeners(
933
}
934
935
export function addEventTypeToDispatchConfig(type: DOMTopLevelEventType): void {
947
- const dispatchConfig = topLevelEventsToDispatchConfig.get(type);
948
- // If we don't have a dispatchConfig, then we're dealing with
936
+ const reactName = topLevelEventsToReactNames.get(type);
937
+ // If we don't have a reactName, then we're dealing with
938
// an event type that React does not know about (i.e. a custom event).
939
// We need to register an event config for this or the SimpleEventPlugin
940
// will not appropriately provide a SyntheticEvent, so we use out empty
941
// dispatch config for custom events.
953
- if (dispatchConfig === undefined) {
954
- topLevelEventsToDispatchConfig.set(
955
- type,
956
- emptyDispatchConfigForCustomEvents,
957
- );
942
+ if (reactName === undefined) {
943
+ topLevelEventsToReactNames.set(type, null);
944
}
945
}
946
packages/react-dom/src/events/EventPluginRegistry.js
+10
-14
@@ -10,13 +10,6 @@
10
import type {TopLevelType} from './TopLevelEventTypes';
11
import type {EventTypes} from './PluginModuleType';
12
13
-import invariant from 'shared/invariant';
14
-
15
-/**
16
- * Mapping from registration name to plugin module
17
- */
18
-export const registrationNames = {};
19
-
13
/**
14
* Mapping from registration name to event name
15
*/
@@ -62,13 +55,16 @@ function publishRegistrationName(
55
registrationName: string,
56
dependencies: ?Array<TopLevelType>,
57
): void {
65
- invariant(
66
- !registrationNames[registrationName],
67
- 'EventPluginRegistry: More than one plugin attempted to publish the same ' +
68
- 'registration name, `%s`.',
69
- registrationName,
70
- );
71
- registrationNames[registrationName] = true;
58
+ if (__DEV__) {
59
+ if (registrationNameDependencies[registrationName]) {
60
+ console.error(
61
+ 'EventPluginRegistry: More than one plugin attempted to publish the same ' +
62
+ 'registration name, `%s`.',
63
+ registrationName,
64
+ );
65
+ }
66
+ }
67
+
68
registrationNameDependencies[registrationName] = dependencies;
69
70
if (__DEV__) {
packages/react-dom/src/events/ReactSyntheticEventType.js
+1
-10
@@ -22,21 +22,12 @@ export type DispatchConfig = {|
22
eventPriority?: EventPriority,
23
|};
24
25
-export type CustomDispatchConfig = {|
26
- phasedRegistrationNames: {|
27
- bubbled: null,
28
- captured: null,
29
- |},
30
- registrationName?: string,
31
- customEvent: true,
32
-|};
33
-
25
export type ReactSyntheticEvent = {|
35
- dispatchConfig: DispatchConfig | CustomDispatchConfig,
26
isPersistent: () => boolean,
27
isPropagationStopped: () => boolean,
28
_dispatchInstances?: null | Array<Fiber | null> | Fiber,
29
_dispatchListeners?: null | Array<Function> | Function,
30
+ _reactName: string,
31
_targetInst: Fiber,
32
type: string,
33
currentTarget: null | EventTarget,
packages/react-dom/src/events/SyntheticEvent.js
+2
-12
@@ -48,19 +48,9 @@ function functionThatReturnsFalse() {
48
* Synthetic events (and subclasses) implement the DOM Level 3 Events API by
49
* normalizing browser quirks. Subclasses do not necessarily have to implement a
50
* DOM interface; custom application-specific events can also subclass this.
51
- *
52
- * @param {object} dispatchConfig Configuration used to dispatch this event.
53
- * @param {*} targetInst Marker identifying the event target.
54
- * @param {object} nativeEvent Native browser event.
55
- * @param {DOMEventTarget} nativeEventTarget Target node.
51
*/
57
-function SyntheticEvent(
58
- dispatchConfig,
59
- targetInst,
60
- nativeEvent,
61
- nativeEventTarget,
62
-) {
63
- this.dispatchConfig = dispatchConfig;
52
+function SyntheticEvent(reactName, targetInst, nativeEvent, nativeEventTarget) {
53
+ this._reactName = reactName;
54
this._targetInst = targetInst;
55
this.nativeEvent = nativeEvent;
56
packages/react-dom/src/events/plugins/ModernBeforeInputEventPlugin.js
+8
-8
@@ -140,11 +140,11 @@ function isKeypressCommand(nativeEvent) {
140
function getCompositionEventType(topLevelType) {
141
switch (topLevelType) {
142
case TOP_COMPOSITION_START:
143
- return eventTypes.compositionStart;
143
+ return 'onCompositionStart';
144
case TOP_COMPOSITION_END:
145
- return eventTypes.compositionEnd;
145
+ return 'onCompositionEnd';
146
case TOP_COMPOSITION_UPDATE:
147
- return eventTypes.compositionUpdate;
147
+ return 'onCompositionUpdate';
148
}
149
}
150
@@ -237,10 +237,10 @@ function extractCompositionEvent(
237
eventType = getCompositionEventType(topLevelType);
238
} else if (!isComposing) {
239
if (isFallbackCompositionStart(topLevelType, nativeEvent)) {
240
- eventType = eventTypes.compositionStart;
240
+ eventType = 'onCompositionStart';
241
}
242
} else if (isFallbackCompositionEnd(topLevelType, nativeEvent)) {
243
- eventType = eventTypes.compositionEnd;
243
+ eventType = 'onCompositionEnd';
244
}
245
246
if (!eventType) {
@@ -250,9 +250,9 @@ function extractCompositionEvent(
250
if (useFallbackCompositionData && !isUsingKoreanIME(nativeEvent)) {
251
// The current composition is stored statically and must not be
252
// overwritten while composition continues.
253
- if (!isComposing && eventType === eventTypes.compositionStart) {
253
+ if (!isComposing && eventType === 'onCompositionStart') {
254
isComposing = FallbackCompositionStateInitialize(nativeEventTarget);
255
- } else if (eventType === eventTypes.compositionEnd) {
255
+ } else if (eventType === 'onCompositionEnd') {
256
if (isComposing) {
257
fallbackData = FallbackCompositionStateGetData();
258
}
@@ -430,7 +430,7 @@ function extractBeforeInputEvent(
430
}
431
432
const event = new SyntheticInputEvent(
433
- eventTypes.beforeInput,
433
+ 'onBeforeInput',
434
null,
435
nativeEvent,
436
nativeEventTarget,
packages/react-dom/src/events/plugins/ModernChangeEventPlugin.js
+1
-6
@@ -64,12 +64,7 @@ function createAndAccumulateChangeEvent(
64
nativeEvent,
65
target,
66
) {
67
- const event = new SyntheticEvent(
68
- eventTypes.change,
69
- null,
70
- nativeEvent,
71
- target,
72
- );
67
+ const event = new SyntheticEvent('onChange', null, nativeEvent, target);
68
event.type = 'change';
69
// Flag this event loop as needing state restore.
70
enqueueStateRestore(((target: any): Node));
packages/react-dom/src/events/plugins/ModernEnterLeaveEventPlugin.js
+4
-4
@@ -126,16 +126,16 @@ function extractEvents(
126
127
if (topLevelType === TOP_MOUSE_OUT || topLevelType === TOP_MOUSE_OVER) {
128
eventInterface = SyntheticMouseEvent;
129
- leaveEventType = eventTypes.mouseLeave;
130
- enterEventType = eventTypes.mouseEnter;
129
+ leaveEventType = 'onMouseLeave';
130
+ enterEventType = 'onMouseEnter';
131
eventTypePrefix = 'mouse';
132
} else if (
133
topLevelType === TOP_POINTER_OUT ||
134
topLevelType === TOP_POINTER_OVER
135
) {
136
eventInterface = SyntheticPointerEvent;
137
- leaveEventType = eventTypes.pointerLeave;
138
- enterEventType = eventTypes.pointerEnter;
137
+ leaveEventType = 'onPointerLeave';
138
+ enterEventType = 'onPointerEnter';
139
eventTypePrefix = 'pointer';
140
}
141
packages/react-dom/src/events/plugins/ModernSelectEventPlugin.js
+1
-1
@@ -133,7 +133,7 @@ function constructSelectEvent(dispatchQueue, nativeEvent, nativeEventTarget) {
133
lastSelection = currentSelection;
134
135
const syntheticEvent = new SyntheticEvent(
136
- eventTypes.select,
136
+ 'onSelect',
137
null,
138
nativeEvent,
139
nativeEventTarget,
packages/react-dom/src/events/plugins/ModernSimpleEventPlugin.js
+6
-57
@@ -7,10 +7,7 @@
7
* @flow
8
*/
9
10
-import type {
11
- TopLevelType,
12
- DOMTopLevelEventType,
13
-} from '../../events/TopLevelEventTypes';
10
+import type {TopLevelType} from '../../events/TopLevelEventTypes';
11
import type {Fiber} from 'react-reconciler/src/ReactInternalTypes';
12
import type {
13
AnyNativeEvent,
@@ -22,7 +19,7 @@ import SyntheticEvent from '../../events/SyntheticEvent';
19
20
import * as DOMTopLevelEventTypes from '../DOMTopLevelEventTypes';
21
import {
25
- topLevelEventsToDispatchConfig,
22
+ topLevelEventsToReactNames,
23
simpleEventPluginEventTypes,
24
} from '../DOMEventProperties';
25
import {
@@ -46,41 +43,6 @@ import getEventCharCode from '../getEventCharCode';
43
44
import {enableCreateEventHandleAPI} from 'shared/ReactFeatureFlags';
45
49
-// Only used in DEV for exhaustiveness validation.
50
-const knownHTMLTopLevelTypes: Array<DOMTopLevelEventType> = [
51
- DOMTopLevelEventTypes.TOP_ABORT,
52
- DOMTopLevelEventTypes.TOP_CANCEL,
53
- DOMTopLevelEventTypes.TOP_CAN_PLAY,
54
- DOMTopLevelEventTypes.TOP_CAN_PLAY_THROUGH,
55
- DOMTopLevelEventTypes.TOP_CLOSE,
56
- DOMTopLevelEventTypes.TOP_DURATION_CHANGE,
57
- DOMTopLevelEventTypes.TOP_EMPTIED,
58
- DOMTopLevelEventTypes.TOP_ENCRYPTED,
59
- DOMTopLevelEventTypes.TOP_ENDED,
60
- DOMTopLevelEventTypes.TOP_ERROR,
61
- DOMTopLevelEventTypes.TOP_INPUT,
62
- DOMTopLevelEventTypes.TOP_INVALID,
63
- DOMTopLevelEventTypes.TOP_LOAD,
64
- DOMTopLevelEventTypes.TOP_LOADED_DATA,
65
- DOMTopLevelEventTypes.TOP_LOADED_METADATA,
66
- DOMTopLevelEventTypes.TOP_LOAD_START,
67
- DOMTopLevelEventTypes.TOP_PAUSE,
68
- DOMTopLevelEventTypes.TOP_PLAY,
69
- DOMTopLevelEventTypes.TOP_PLAYING,
70
- DOMTopLevelEventTypes.TOP_PROGRESS,
71
- DOMTopLevelEventTypes.TOP_RATE_CHANGE,
72
- DOMTopLevelEventTypes.TOP_RESET,
73
- DOMTopLevelEventTypes.TOP_SEEKED,
74
- DOMTopLevelEventTypes.TOP_SEEKING,
75
- DOMTopLevelEventTypes.TOP_STALLED,
76
- DOMTopLevelEventTypes.TOP_SUBMIT,
77
- DOMTopLevelEventTypes.TOP_SUSPEND,
78
- DOMTopLevelEventTypes.TOP_TIME_UPDATE,
79
- DOMTopLevelEventTypes.TOP_TOGGLE,
80
- DOMTopLevelEventTypes.TOP_VOLUME_CHANGE,
81
- DOMTopLevelEventTypes.TOP_WAITING,
82
-];
83
-
46
function extractEvents(
47
dispatchQueue: DispatchQueue,
48
topLevelType: TopLevelType,
@@ -90,8 +52,8 @@ function extractEvents(
52
eventSystemFlags: EventSystemFlags,
53
targetContainer: null | EventTarget,
54
): void {
93
- const dispatchConfig = topLevelEventsToDispatchConfig.get(topLevelType);
94
- if (!dispatchConfig) {
55
+ const reactName = topLevelEventsToReactNames.get(topLevelType);
56
+ if (reactName === undefined) {
57
return;
58
}
59
let EventConstructor;
@@ -179,25 +141,12 @@ function extractEvents(
141
EventConstructor = SyntheticPointerEvent;
142
break;
143
default:
182
- if (__DEV__) {
183
- if (
184
- knownHTMLTopLevelTypes.indexOf(topLevelType) === -1 &&
185
- dispatchConfig.customEvent !== true
186
- ) {
187
- console.error(
188
- 'SimpleEventPlugin: Unhandled event type, `%s`. This warning ' +
189
- 'is likely caused by a bug in React. Please file an issue.',
190
- topLevelType,
191
- );
192
- }
193
- }
194
- // HTML Events
195
- // @see http://www.w3.org/TR/html5/index.html#events-0
144
+ // Unknown event. This is used by createEventHandle.
145
EventConstructor = SyntheticEvent;
146
break;
147
}
148
const event = new EventConstructor(
200
- dispatchConfig,
149
+ reactName,
150
null,
151
nativeEvent,
152
nativeEventTarget,
packages/react-dom/src/shared/ReactDOMUnknownPropertyHook.js
+5
-2
@@ -43,8 +43,11 @@ if (__DEV__) {
43
44
// We can't rely on the event system being injected on the server.
45
if (eventRegistry != null) {
46
- const {registrationNames, possibleRegistrationNames} = eventRegistry;
47
- if (registrationNames.hasOwnProperty(name)) {
46
+ const {
47
+ registrationNameDependencies,
48
+ possibleRegistrationNames,
49
+ } = eventRegistry;
50
+ if (registrationNameDependencies.hasOwnProperty(name)) {
51
return true;
52
}
53
const registrationName = possibleRegistrationNames.hasOwnProperty(
packages/react-dom/src/test-utils/ReactTestUtils.js
+13
-24
@@ -491,14 +491,16 @@ function getListener(inst: Fiber, registrationName: string) {
491
}
492
493
function listenerAtPhase(inst, event, propagationPhase: PropagationPhases) {
494
- const registrationName =
495
- event.dispatchConfig.phasedRegistrationNames[propagationPhase];
494
+ let registrationName = event._reactName;
495
+ if (propagationPhase === 'captured') {
496
+ registrationName += 'Capture';
497
+ }
498
return getListener(inst, registrationName);
499
}
500
501
function accumulateDispatches(inst, ignoredDirection, event) {
500
- if (inst && event && event.dispatchConfig.registrationName) {
501
- const registrationName = event.dispatchConfig.registrationName;
502
+ if (inst && event && event._reactName) {
503
+ const registrationName = event._reactName;
504
const listener = getListener(inst, registrationName);
505
if (listener) {
506
if (event._dispatchListeners == null) {
@@ -533,13 +535,13 @@ function accumulateDirectionalDispatches(inst, phase, event) {
535
}
536
537
function accumulateDirectDispatchesSingle(event) {
536
- if (event && event.dispatchConfig.registrationName) {
538
+ if (event && event._reactName) {
539
accumulateDispatches(event._targetInst, null, event);
540
}
541
}
542
543
function accumulateTwoPhaseDispatchesSingle(event) {
542
- if (event && event.dispatchConfig.phasedRegistrationNames) {
544
+ if (event && event._reactName) {
545
traverseTwoPhase(event._targetInst, accumulateDirectionalDispatches, event);
546
}
547
}
@@ -577,27 +579,14 @@ function makeSimulator(eventType) {
579
'a component instance. Pass the DOM node you wish to simulate the event on instead.',
580
);
581
580
- // Reconstruct more or less what the original event system produced.
581
- // We could remove this indirection here but we also don't plan to invest in Simulate anyway.
582
- const dispatchConfig = {};
583
- if (directDispatchEventTypes.has(eventType)) {
584
- dispatchConfig.registrationName =
585
- 'on' + eventType[0].toUpperCase() + eventType.slice(1);
586
- } else {
587
- dispatchConfig.phasedRegistrationNames = {
588
- bubbled: 'on' + eventType[0].toUpperCase() + eventType.slice(1),
589
- captured:
590
- 'on' + eventType[0].toUpperCase() + eventType.slice(1) + 'Capture',
591
- };
592
- }
593
-
582
+ const reactName = 'on' + eventType[0].toUpperCase() + eventType.slice(1);
583
const fakeNativeEvent = new Event();
584
fakeNativeEvent.target = domNode;
585
fakeNativeEvent.type = eventType.toLowerCase();
586
587
const targetInst = getInstanceFromNode(domNode);
588
const event = new SyntheticEvent(
600
- dispatchConfig,
589
+ reactName,
590
targetInst,
591
fakeNativeEvent,
592
domNode,
@@ -608,10 +597,10 @@ function makeSimulator(eventType) {
597
event.persist();
598
Object.assign(event, eventData);
599
611
- if (dispatchConfig.phasedRegistrationNames) {
612
- accumulateTwoPhaseDispatchesSingle(event);
613
- } else {
600
+ if (directDispatchEventTypes.has(eventType)) {
601
accumulateDirectDispatchesSingle(event);
602
+ } else {
603
+ accumulateTwoPhaseDispatchesSingle(event);
604
}
605
606
ReactDOM.unstable_batchedUpdates(function() {