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 14 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
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;
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