Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add infrastructure for passive/non-passive event support for future API exploration #15036

Merged
merged 20 commits into from
Mar 15, 2019
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 24 additions & 12 deletions packages/events/EventPluginHub.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ import type {Fiber} from 'react-reconciler/src/ReactFiber';
import type {AnyNativeEvent} from './PluginModuleType';
import type {TopLevelType} from './TopLevelEventTypes';

import {type ListenerType, PASSIVE_DISABLED} from 'events/ListenerTypes';

/**
* Internal queue of events that have accumulated their dispatches and are
* waiting to have their dispatches executed.
Expand Down Expand Up @@ -163,23 +165,31 @@ function extractEvents(
targetInst: null | Fiber,
nativeEvent: AnyNativeEvent,
nativeEventTarget: EventTarget,
listenerType: ListenerType,
): Array<ReactSyntheticEvent> | ReactSyntheticEvent | null {
let events = null;
for (let i = 0; i < plugins.length; i++) {
// Not every plugin in the ordering may be loaded at runtime.
const possiblePlugin: PluginModule<AnyNativeEvent> = plugins[i];
if (possiblePlugin) {
const extractedEvents = possiblePlugin.extractEvents(
topLevelType,
targetInst,
nativeEvent,
nativeEventTarget,
);
if (extractedEvents) {
events = accumulateInto(events, extractedEvents);
// For events that don't use the new passive event type system,
// we continue to use event plugins. This will get updated once
// we add plugins or adapters that make use of the passive event
// system.
if (listenerType === PASSIVE_DISABLED) {
for (let i = 0; i < plugins.length; i++) {
// Not every plugin in the ordering may be loaded at runtime.
const possiblePlugin: PluginModule<AnyNativeEvent> = plugins[i];
if (possiblePlugin) {
const extractedEvents = possiblePlugin.extractEvents(
topLevelType,
targetInst,
nativeEvent,
nativeEventTarget,
);
if (extractedEvents) {
events = accumulateInto(events, extractedEvents);
}
}
}
}

return events;
}

Expand Down Expand Up @@ -214,12 +224,14 @@ export function runExtractedEventsInBatch(
targetInst: null | Fiber,
nativeEvent: AnyNativeEvent,
nativeEventTarget: EventTarget,
listenerType: ListenerType,
) {
const events = extractEvents(
topLevelType,
targetInst,
nativeEvent,
nativeEventTarget,
listenerType,
);
runEventsInBatch(events);
}
15 changes: 15 additions & 0 deletions packages/events/ListenerTypes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/

export const PASSIVE_DISABLED = 0;
export const PASSIVE_FALLBACK = 1;
export const PASSIVE_TRUE = 2;
export const PASSIVE_FALSE = 3;

export type ListenerType = 0 | 1 | 2 | 3;
1 change: 1 addition & 0 deletions packages/events/PluginModuleType.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export type PluginModule<NativeEvent> = {
targetInst: null | Fiber,
nativeTarget: NativeEvent,
nativeEventTarget: EventTarget,
passive?: null | boolean,
) => ?ReactSyntheticEvent,
tapMoveThreshold?: number,
};
8 changes: 4 additions & 4 deletions packages/events/ReactGenericBatching.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ import {
let _batchedUpdatesImpl = function(fn, bookkeeping) {
return fn(bookkeeping);
};
let _interactiveUpdatesImpl = function(fn, a, b) {
return fn(a, b);
let _interactiveUpdatesImpl = function(fn, a, b, c) {
return fn(a, b, c);
};
let _flushInteractiveUpdatesImpl = function() {};

Expand Down Expand Up @@ -52,8 +52,8 @@ export function batchedUpdates(fn, bookkeeping) {
}
}

export function interactiveUpdates(fn, a, b) {
return _interactiveUpdatesImpl(fn, a, b);
export function interactiveUpdates(fn, a, b, c) {
return _interactiveUpdatesImpl(fn, a, b, c);
}

export function flushInteractiveUpdates() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -331,15 +331,15 @@ describe('ReactBrowserEventEmitter', () => {

it('should listen to events only once', () => {
spyOnDevAndProd(EventTarget.prototype, 'addEventListener');
ReactBrowserEventEmitter.listenTo(ON_CLICK_KEY, document);
ReactBrowserEventEmitter.listenTo(ON_CLICK_KEY, document);
ReactBrowserEventEmitter.listenTo(ON_CLICK_KEY, document, true);
ReactBrowserEventEmitter.listenTo(ON_CLICK_KEY, document, true);
expect(EventTarget.prototype.addEventListener).toHaveBeenCalledTimes(1);
});

it('should work with event plugins without dependencies', () => {
spyOnDevAndProd(EventTarget.prototype, 'addEventListener');

ReactBrowserEventEmitter.listenTo(ON_CLICK_KEY, document);
ReactBrowserEventEmitter.listenTo(ON_CLICK_KEY, document, true);

expect(EventTarget.prototype.addEventListener.calls.argsFor(0)[0]).toBe(
'click',
Expand All @@ -349,7 +349,7 @@ describe('ReactBrowserEventEmitter', () => {
it('should work with event plugins with dependencies', () => {
spyOnDevAndProd(EventTarget.prototype, 'addEventListener');

ReactBrowserEventEmitter.listenTo(ON_CHANGE_KEY, document);
ReactBrowserEventEmitter.listenTo(ON_CHANGE_KEY, document, true);

const setEventListeners = [];
const listenCalls = EventTarget.prototype.addEventListener.calls.allArgs();
Expand Down
51 changes: 27 additions & 24 deletions packages/react-dom/src/client/ReactDOMComponent.js
Original file line number Diff line number Diff line change
Expand Up @@ -253,14 +253,17 @@ if (__DEV__) {
};
}

function ensureListeningTo(rootContainerElement, registrationName) {
function ensureListeningTo(
rootContainerElement: Element | Node,
registrationName: string,
): void {
const isDocumentOrFragment =
rootContainerElement.nodeType === DOCUMENT_NODE ||
rootContainerElement.nodeType === DOCUMENT_FRAGMENT_NODE;
const doc = isDocumentOrFragment
? rootContainerElement
: rootContainerElement.ownerDocument;
listenTo(registrationName, doc);
listenTo(registrationName, doc, true /* isLegacy */);
}

function getOwnerDocumentFromRootContainer(
Expand Down Expand Up @@ -494,41 +497,41 @@ export function setInitialProperties(
switch (tag) {
case 'iframe':
case 'object':
trapBubbledEvent(TOP_LOAD, domElement);
trapBubbledEvent(TOP_LOAD, domElement, true);
props = rawProps;
break;
case 'video':
case 'audio':
// Create listener for each media event
for (let i = 0; i < mediaEventTypes.length; i++) {
trapBubbledEvent(mediaEventTypes[i], domElement);
trapBubbledEvent(mediaEventTypes[i], domElement, true);
}
props = rawProps;
break;
case 'source':
trapBubbledEvent(TOP_ERROR, domElement);
trapBubbledEvent(TOP_ERROR, domElement, true);
props = rawProps;
break;
case 'img':
case 'image':
case 'link':
trapBubbledEvent(TOP_ERROR, domElement);
trapBubbledEvent(TOP_LOAD, domElement);
trapBubbledEvent(TOP_ERROR, domElement, true);
trapBubbledEvent(TOP_LOAD, domElement, true);
props = rawProps;
break;
case 'form':
trapBubbledEvent(TOP_RESET, domElement);
trapBubbledEvent(TOP_SUBMIT, domElement);
trapBubbledEvent(TOP_RESET, domElement, true);
trapBubbledEvent(TOP_SUBMIT, domElement, true);
props = rawProps;
break;
case 'details':
trapBubbledEvent(TOP_TOGGLE, domElement);
trapBubbledEvent(TOP_TOGGLE, domElement, true);
props = rawProps;
break;
case 'input':
ReactDOMInputInitWrapperState(domElement, rawProps);
props = ReactDOMInputGetHostProps(domElement, rawProps);
trapBubbledEvent(TOP_INVALID, domElement);
trapBubbledEvent(TOP_INVALID, domElement, true);
Copy link
Collaborator

@gaearon gaearon Mar 14, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Per our discussion, it seems like these code paths will never "want" to get two listeners attached/invoked. So instead of branching deeply and passing an argument through, let's fork the innermost implementation and call the fork from the new code when we want to. That will also likely let us DCE more based on a feature flag.

// For controlled components we always need to ensure we're listening
// to onChange. Even if there is no listener.
ensureListeningTo(rootContainerElement, 'onChange');
Expand All @@ -540,15 +543,15 @@ export function setInitialProperties(
case 'select':
ReactDOMSelectInitWrapperState(domElement, rawProps);
props = ReactDOMSelectGetHostProps(domElement, rawProps);
trapBubbledEvent(TOP_INVALID, domElement);
trapBubbledEvent(TOP_INVALID, domElement, true);
// For controlled components we always need to ensure we're listening
// to onChange. Even if there is no listener.
ensureListeningTo(rootContainerElement, 'onChange');
break;
case 'textarea':
ReactDOMTextareaInitWrapperState(domElement, rawProps);
props = ReactDOMTextareaGetHostProps(domElement, rawProps);
trapBubbledEvent(TOP_INVALID, domElement);
trapBubbledEvent(TOP_INVALID, domElement, true);
// For controlled components we always need to ensure we're listening
// to onChange. Even if there is no listener.
ensureListeningTo(rootContainerElement, 'onChange');
Expand Down Expand Up @@ -888,34 +891,34 @@ export function diffHydratedProperties(
switch (tag) {
case 'iframe':
case 'object':
trapBubbledEvent(TOP_LOAD, domElement);
trapBubbledEvent(TOP_LOAD, domElement, true);
break;
case 'video':
case 'audio':
// Create listener for each media event
for (let i = 0; i < mediaEventTypes.length; i++) {
trapBubbledEvent(mediaEventTypes[i], domElement);
trapBubbledEvent(mediaEventTypes[i], domElement, true);
}
break;
case 'source':
trapBubbledEvent(TOP_ERROR, domElement);
trapBubbledEvent(TOP_ERROR, domElement, true);
break;
case 'img':
case 'image':
case 'link':
trapBubbledEvent(TOP_ERROR, domElement);
trapBubbledEvent(TOP_LOAD, domElement);
trapBubbledEvent(TOP_ERROR, domElement, true);
trapBubbledEvent(TOP_LOAD, domElement, true);
break;
case 'form':
trapBubbledEvent(TOP_RESET, domElement);
trapBubbledEvent(TOP_SUBMIT, domElement);
trapBubbledEvent(TOP_RESET, domElement, true);
trapBubbledEvent(TOP_SUBMIT, domElement, true);
break;
case 'details':
trapBubbledEvent(TOP_TOGGLE, domElement);
trapBubbledEvent(TOP_TOGGLE, domElement, true);
break;
case 'input':
ReactDOMInputInitWrapperState(domElement, rawProps);
trapBubbledEvent(TOP_INVALID, domElement);
trapBubbledEvent(TOP_INVALID, domElement, true);
// For controlled components we always need to ensure we're listening
// to onChange. Even if there is no listener.
ensureListeningTo(rootContainerElement, 'onChange');
Expand All @@ -925,14 +928,14 @@ export function diffHydratedProperties(
break;
case 'select':
ReactDOMSelectInitWrapperState(domElement, rawProps);
trapBubbledEvent(TOP_INVALID, domElement);
trapBubbledEvent(TOP_INVALID, domElement, true);
// For controlled components we always need to ensure we're listening
// to onChange. Even if there is no listener.
ensureListeningTo(rootContainerElement, 'onChange');
break;
case 'textarea':
ReactDOMTextareaInitWrapperState(domElement, rawProps);
trapBubbledEvent(TOP_INVALID, domElement);
trapBubbledEvent(TOP_INVALID, domElement, true);
// For controlled components we always need to ensure we're listening
// to onChange. Even if there is no listener.
ensureListeningTo(rootContainerElement, 'onChange');
Expand Down
31 changes: 27 additions & 4 deletions packages/react-dom/src/events/EventListener.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,41 @@
* @flow
*/

import {
type ListenerType,
PASSIVE_DISABLED,
PASSIVE_FALLBACK,
PASSIVE_TRUE,
} from 'events/ListenerTypes';

export function addEventBubbleListener(
element: Document | Element,
element: Document | Element | Node,
eventType: string,
listener: Function,
listenerType: ListenerType,
): void {
element.addEventListener(eventType, listener, false);
if (listenerType === PASSIVE_DISABLED || listenerType === PASSIVE_FALLBACK) {
element.addEventListener(eventType, listener, false);
} else {
element.addEventListener(eventType, listener, {
passive: listenerType === PASSIVE_TRUE,
capture: false,
});
trueadm marked this conversation as resolved.
Show resolved Hide resolved
}
}

export function addEventCaptureListener(
element: Document | Element,
element: Document | Element | Node,
eventType: string,
listener: Function,
listenerType: ListenerType,
): void {
element.addEventListener(eventType, listener, true);
if (listenerType === PASSIVE_DISABLED || listenerType === PASSIVE_FALLBACK) {
element.addEventListener(eventType, listener, true);
} else {
element.addEventListener(eventType, listener, {
passive: listenerType === PASSIVE_TRUE,
capture: true,
});
}
}
Loading