From a33b351f4781686872d34a3e61d75cc71ba4365d Mon Sep 17 00:00:00 2001 From: Joshua Gross Date: Thu, 10 Feb 2022 17:36:27 -0800 Subject: [PATCH 01/27] Turn HostComponent into an EventEmitter --- .../react-native-renderer/src/CustomEvent.js | 40 ++++++++++ .../src/ReactFabricHostConfig.js | 72 ++++++++++++++++++ .../src/ReactNativeBridgeEventPlugin.js | 8 +- .../src/ReactNativeGetListener.js | 76 ++++++++++++++++--- .../src/legacy-events/PropagationPhases.js | 10 +++ 5 files changed, 191 insertions(+), 15 deletions(-) create mode 100644 packages/react-native-renderer/src/CustomEvent.js create mode 100644 packages/react-native-renderer/src/legacy-events/PropagationPhases.js diff --git a/packages/react-native-renderer/src/CustomEvent.js b/packages/react-native-renderer/src/CustomEvent.js new file mode 100644 index 0000000000000..081f2441dd24a --- /dev/null +++ b/packages/react-native-renderer/src/CustomEvent.js @@ -0,0 +1,40 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @format + * @flow strict-local + */ + +export type CustomEventOptions = $ReadOnly<{| + bubbles?: boolean, + cancelable?: boolean, + detail?: { ... } +|}>; + +// TODO: should extend an Event base-class that has most of these properties +class CustomEvent extends Event { + type: string; + detail: ?{ ... }; + bubbles: boolean; + cancelable: boolean; + isTrusted: boolean; + + constructor(typeArg: string, options: CustomEventOptions) { + const { bubbles, cancelable } = options; + // TODO: support passing in the `composed` param + super(typeArg, { bubbles, cancelable }); + + this.detail = options.detail; // this would correspond to `NativeEvent` in SyntheticEvent + + // TODO: do we need these since they should be on Event? (for RN we probably need a polyfill) + this.type = typeArg; + this.bubbles = !!(bubbles || false); + this.cancelable = !!(cancelable || false); + this.isTrusted = false; + } +} + +export default CustomEvent; diff --git a/packages/react-native-renderer/src/ReactFabricHostConfig.js b/packages/react-native-renderer/src/ReactFabricHostConfig.js index bec60bff22c99..5d7cbe629c027 100644 --- a/packages/react-native-renderer/src/ReactFabricHostConfig.js +++ b/packages/react-native-renderer/src/ReactFabricHostConfig.js @@ -23,6 +23,7 @@ import {mountSafeCallback_NOT_REALLY_SAFE} from './NativeMethodsMixinUtils'; import {create, diff} from './ReactNativeAttributePayload'; import {dispatchEvent} from './ReactFabricEventEmitter'; +import CustomEvent from './CustomEvent'; import { DefaultEventPriority, @@ -95,6 +96,19 @@ export type RendererInspectionConfig = $ReadOnly<{| ) => void, |}>; +// TODO: find a better place for this type to live +export type EventListenerOptions = $ReadOnly<{| + capture?: boolean, + once?: boolean, + passive?: boolean, + signal: mixed, // not yet implemented +|}>; +export type EventListenerRemoveOptions = $ReadOnly<{| + capture?: boolean, +|}>; +// TODO: this will be changed in the future to be w3c-compatible and allow "EventListener" objects as well as functions. +export type EventListener = Function; + // TODO: Remove this conditional once all changes have propagated. if (registerEventHandler) { /** @@ -111,6 +125,7 @@ class ReactFabricHostComponent { viewConfig: ViewConfig; currentProps: Props; _internalInstanceHandle: Object; + _eventListeners: { [string]: $ReadOnly<{| listener: EventListener, options: EventListenerOptions |}>[] }; constructor( tag: number, @@ -122,6 +137,7 @@ class ReactFabricHostComponent { this.viewConfig = viewConfig; this.currentProps = props; this._internalInstanceHandle = internalInstanceHandle; + this._eventListeners = {}; } blur() { @@ -193,6 +209,62 @@ class ReactFabricHostComponent { return; } + + dipatchEvent_unstable(event: CustomEvent) { + dispatchEvent(this._internalInstanceHandle, event.type, event); + } + + // This API adheres to the w3c addEventListener spec. + // See https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener + // See https://www.w3.org/TR/DOM-Level-2-Events/events.html + // + // Exceptions/TODOs: + // (1) listener must currently be a function, we do not support EventListener objects yet. + // (2) we do not support the `signal` option / AbortSignal yet + addEventListener_unstable(eventType: string, listener: EventListener, options: EventListenerOptions | boolean) { + if (typeof eventType !== 'string') { + throw new Error('addEventListener_unstable eventType must be a string'); + } + if (typeof listener !== 'function') { + throw new Error('addEventListener_unstable listener must be a function'); + } + + // The third argument is either boolean indicating "captures" or an object. + const optionsObj = (typeof options === 'object' && options !== null ? options : {}); + const capture = (typeof options === 'boolean' ? options : (optionsObj.capture)) || false; + const once = optionsObj.once || false; + const passive = optionsObj.passive || false; + const signal = null; // TODO: implement signal/AbortSignal + + this._eventListeners[eventType] = this._eventListeners[eventType] || []; + this._eventListeners[eventType].push({ + listener: listener, + options: { + capture: capture, + once: once, + passive: passive, + signal: signal + } + }); + } + + // See https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/removeEventListener + removeEventListener_unstable(eventType: string, listener: EventListener, options: EventListenerRemoveOptions | boolean) { + // eventType and listener must be referentially equal to be removed from the listeners + // data structure, but in "options" we only check the `capture` flag, according to spec. + // That means if you add the same function as a listener with capture set to true and false, + // you must also call removeEventListener twice with capture set to true/false. + const optionsObj = (typeof options === 'object' && options !== null ? options : {}); + const capture = (typeof options === 'boolean' ? options : (optionsObj.capture)) || false; + + if (!this._eventListeners[eventType]) { + return; + } + + this._eventListeners[eventType] = this._eventListeners[eventType].filter(listenerObj => { + return !(listenerObj.listener === listener && listenerObj.options.capture === capture); + }); + }; } // eslint-disable-next-line no-unused-expressions diff --git a/packages/react-native-renderer/src/ReactNativeBridgeEventPlugin.js b/packages/react-native-renderer/src/ReactNativeBridgeEventPlugin.js index 212b181c08294..c10b5233369c5 100644 --- a/packages/react-native-renderer/src/ReactNativeBridgeEventPlugin.js +++ b/packages/react-native-renderer/src/ReactNativeBridgeEventPlugin.js @@ -10,6 +10,7 @@ import type {AnyNativeEvent} from './legacy-events/PluginModuleType'; import type {TopLevelType} from './legacy-events/TopLevelEventTypes'; import SyntheticEvent from './legacy-events/SyntheticEvent'; +import type {PropagationPhases} from './legacy-events/PropagationPhases'; // Module provided by RN: import {ReactNativeViewConfigRegistry} from 'react-native/Libraries/ReactPrivate/ReactNativePrivateInterface'; @@ -29,7 +30,7 @@ const { function listenerAtPhase(inst, event, propagationPhase: PropagationPhases) { const registrationName = event.dispatchConfig.phasedRegistrationNames[propagationPhase]; - return getListener(inst, registrationName); + return getListener(inst, registrationName, propagationPhase); } function accumulateDirectionalDispatches(inst, phase, event) { @@ -103,7 +104,9 @@ function accumulateDispatches( ): void { if (inst && event && event.dispatchConfig.registrationName) { const registrationName = event.dispatchConfig.registrationName; - const listener = getListener(inst, registrationName); + // Since we "do not look for phased registration names", that + // should be the same as "bubbled" here, for all intents and purposes...? + const listener = getListener(inst, registrationName, 'bubbled'); if (listener) { event._dispatchListeners = accumulateInto( event._dispatchListeners, @@ -130,7 +133,6 @@ function accumulateDirectDispatches(events: ?(Array | Object)) { } // End of inline -type PropagationPhases = 'bubbled' | 'captured'; const ReactNativeBridgeEventPlugin = { eventTypes: {}, diff --git a/packages/react-native-renderer/src/ReactNativeGetListener.js b/packages/react-native-renderer/src/ReactNativeGetListener.js index 4f76fddd29e7b..0f429f6ee45a2 100644 --- a/packages/react-native-renderer/src/ReactNativeGetListener.js +++ b/packages/react-native-renderer/src/ReactNativeGetListener.js @@ -7,30 +7,82 @@ */ import type {Fiber} from 'react-reconciler/src/ReactInternalTypes'; +import type {PropagationPhases} from './legacy-events/PropagationPhases'; import {getFiberCurrentPropsFromNode} from './legacy-events/EventPluginUtils'; export default function getListener( inst: Fiber, registrationName: string, + phase: PropagationPhases, ): Function | null { + // Previously, there was only one possible listener for an event: + // the onEventName property in props. + // Now, it is also possible to have N listeners + // for a specific event on a node. Thus, we accumulate all of the listeners, + // including the props listener, and return a function that calls them all in + // order, starting with the handler prop and then the listeners in order. + // We still return a single function or null. + const listeners = []; + const stateNode = inst.stateNode; - if (stateNode === null) { - // Work in progress (ex: onload events in incremental mode). - return null; + // If null: Work in progress (ex: onload events in incremental mode). + if (stateNode !== null) { + const props = getFiberCurrentPropsFromNode(stateNode); + if (props === null) { + // Work in progress. + return null; + } + const listener = props[registrationName]; + + if (listener && typeof listener !== 'function') { + throw new Error( + `Expected \`${registrationName}\` listener to be a function, instead got a value of \`${typeof listener}\` type.`, + ); + } + + if (listener) { + listeners.push(listener); + } + } + + // Get imperative event listeners for this event + if (stateNode.canonical._eventListeners[registrationName] && stateNode.canonical._eventListeners[registrationName].length > 0) { + var toRemove = []; + for (var listenerObj of stateNode.canonical._eventListeners[registrationName]) { + // Make sure phase of listener matches requested phase + const isCaptureEvent = listenerObj.options.capture != null && listenerObj.options.capture; + if (isCaptureEvent !== (phase === 'captured')) { + continue; + } + + listeners.push(listenerObj.listener); + + // Only call once? If so, remove from listeners after calling. + if (listenerObj.options.once) { + toRemove.push(listenerObj); + } + } + for (var listenerObj of toRemove) { + stateNode.canonical.removeEventListener_unstable(registrationName, listenerObj.listener, listenerObj.capture); + } } - const props = getFiberCurrentPropsFromNode(stateNode); - if (props === null) { - // Work in progress. + + if (listeners.length === 0) { return null; } - const listener = props[registrationName]; - if (listener && typeof listener !== 'function') { - throw new Error( - `Expected \`${registrationName}\` listener to be a function, instead got a value of \`${typeof listener}\` type.`, - ); + if (listeners.length === 1) { + return listeners[0]; } - return listener; + // We need to call at least 2 event handlers + return function () { + // any arguments that are passed to the event handlers + var args = Array.prototype.slice.call(arguments); + + for (var i = 0; i < listeners.length; i++) { + listeners[i] && listeners[i].apply(null, args); + } + }; } diff --git a/packages/react-native-renderer/src/legacy-events/PropagationPhases.js b/packages/react-native-renderer/src/legacy-events/PropagationPhases.js new file mode 100644 index 0000000000000..7d05d30be8c47 --- /dev/null +++ b/packages/react-native-renderer/src/legacy-events/PropagationPhases.js @@ -0,0 +1,10 @@ +/** + * 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 type PropagationPhases = 'bubbled' | 'captured'; From 83e9bc9c6d6a763c68c78e94c7a2b013e165369b Mon Sep 17 00:00:00 2001 From: Joshua Gross Date: Fri, 11 Feb 2022 13:55:04 -0800 Subject: [PATCH 02/27] _eventListeners is null by default to save on memory/perf --- .../src/ReactFabricHostConfig.js | 32 ++++++++++++------- .../src/ReactNativeGetListener.js | 2 +- 2 files changed, 21 insertions(+), 13 deletions(-) diff --git a/packages/react-native-renderer/src/ReactFabricHostConfig.js b/packages/react-native-renderer/src/ReactFabricHostConfig.js index 5d7cbe629c027..526aa3d48afb9 100644 --- a/packages/react-native-renderer/src/ReactFabricHostConfig.js +++ b/packages/react-native-renderer/src/ReactFabricHostConfig.js @@ -117,6 +117,8 @@ if (registerEventHandler) { registerEventHandler(dispatchEvent); } +type InternalEventListeners = { [string]: $ReadOnly<{| listener: EventListener, options: EventListenerOptions |}>[] }; + /** * This is used for refs on host components. */ @@ -125,7 +127,7 @@ class ReactFabricHostComponent { viewConfig: ViewConfig; currentProps: Props; _internalInstanceHandle: Object; - _eventListeners: { [string]: $ReadOnly<{| listener: EventListener, options: EventListenerOptions |}>[] }; + _eventListeners: ?InternalEventListeners; constructor( tag: number, @@ -137,7 +139,7 @@ class ReactFabricHostComponent { this.viewConfig = viewConfig; this.currentProps = props; this._internalInstanceHandle = internalInstanceHandle; - this._eventListeners = {}; + this._eventListeners = null; } blur() { @@ -236,16 +238,22 @@ class ReactFabricHostComponent { const passive = optionsObj.passive || false; const signal = null; // TODO: implement signal/AbortSignal - this._eventListeners[eventType] = this._eventListeners[eventType] || []; - this._eventListeners[eventType].push({ - listener: listener, - options: { - capture: capture, - once: once, - passive: passive, - signal: signal - } - }); + if (this._eventListeners === null) { + this._eventListeners = {}; + } + const eventListeners: InternalEventListeners = this._eventListeners; + if (eventListeners != null) { + eventListeners[eventType] = eventListeners[eventType] || []; + eventListeners[eventType].push({ + listener: listener, + options: { + capture: capture, + once: once, + passive: passive, + signal: signal + } + }); + } } // See https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/removeEventListener diff --git a/packages/react-native-renderer/src/ReactNativeGetListener.js b/packages/react-native-renderer/src/ReactNativeGetListener.js index 0f429f6ee45a2..5d93481de210e 100644 --- a/packages/react-native-renderer/src/ReactNativeGetListener.js +++ b/packages/react-native-renderer/src/ReactNativeGetListener.js @@ -47,7 +47,7 @@ export default function getListener( } // Get imperative event listeners for this event - if (stateNode.canonical._eventListeners[registrationName] && stateNode.canonical._eventListeners[registrationName].length > 0) { + if (stateNode.canonical?._eventListeners?.[registrationName]?.length > 0) { var toRemove = []; for (var listenerObj of stateNode.canonical._eventListeners[registrationName]) { // Make sure phase of listener matches requested phase From d73dd66ce38a52230f1b82b850fe7d9abc45ae00 Mon Sep 17 00:00:00 2001 From: Joshua Gross Date: Fri, 11 Feb 2022 15:32:34 -0800 Subject: [PATCH 03/27] make listeners nullable --- .../src/ReactFabricHostConfig.js | 40 ++++++++++++------- .../src/ReactNativeGetListener.js | 2 +- 2 files changed, 26 insertions(+), 16 deletions(-) diff --git a/packages/react-native-renderer/src/ReactFabricHostConfig.js b/packages/react-native-renderer/src/ReactFabricHostConfig.js index 526aa3d48afb9..6f0ecf3d9784b 100644 --- a/packages/react-native-renderer/src/ReactFabricHostConfig.js +++ b/packages/react-native-renderer/src/ReactFabricHostConfig.js @@ -238,22 +238,25 @@ class ReactFabricHostComponent { const passive = optionsObj.passive || false; const signal = null; // TODO: implement signal/AbortSignal + const eventListeners: InternalEventListeners = this._eventListeners || {}; if (this._eventListeners === null) { - this._eventListeners = {}; + this._eventListeners = eventListeners; } - const eventListeners: InternalEventListeners = this._eventListeners; - if (eventListeners != null) { - eventListeners[eventType] = eventListeners[eventType] || []; - eventListeners[eventType].push({ - listener: listener, - options: { - capture: capture, - once: once, - passive: passive, - signal: signal - } - }); + + const namedEventListeners = eventListeners[eventType] || []; + if (eventListeners[eventType] == null) { + eventListeners[eventType] = namedEventListeners; } + + namedEventListeners.push({ + listener: listener, + options: { + capture: capture, + once: once, + passive: passive, + signal: signal + } + }); } // See https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/removeEventListener @@ -265,11 +268,18 @@ class ReactFabricHostComponent { const optionsObj = (typeof options === 'object' && options !== null ? options : {}); const capture = (typeof options === 'boolean' ? options : (optionsObj.capture)) || false; - if (!this._eventListeners[eventType]) { + // If there are no event listeners or named event listeners, we can bail early - our + // job is already done. + const eventListeners = this._eventListeners; + if (!eventListeners) { + return; + } + const namedEventListeners = eventListeners[eventType]; + if (!namedEventListeners) { return; } - this._eventListeners[eventType] = this._eventListeners[eventType].filter(listenerObj => { + eventListeners[eventType] = namedEventListeners.filter(listenerObj => { return !(listenerObj.listener === listener && listenerObj.options.capture === capture); }); }; diff --git a/packages/react-native-renderer/src/ReactNativeGetListener.js b/packages/react-native-renderer/src/ReactNativeGetListener.js index 5d93481de210e..85314105b1cb9 100644 --- a/packages/react-native-renderer/src/ReactNativeGetListener.js +++ b/packages/react-native-renderer/src/ReactNativeGetListener.js @@ -47,7 +47,7 @@ export default function getListener( } // Get imperative event listeners for this event - if (stateNode.canonical?._eventListeners?.[registrationName]?.length > 0) { + if (stateNode.canonical && stateNode.canonical._eventListeners && stateNode.canonical._eventListeners[registrationName] && stateNode.canonical._eventListeners[registrationName].length > 0) { var toRemove = []; for (var listenerObj of stateNode.canonical._eventListeners[registrationName]) { // Make sure phase of listener matches requested phase From d35b766f1b7261fef92ded3c1287c92d428b776f Mon Sep 17 00:00:00 2001 From: Joshua Gross Date: Fri, 11 Feb 2022 16:58:21 -0800 Subject: [PATCH 04/27] New mechanism for 'once' --- .../src/ReactFabricHostConfig.js | 3 ++- .../src/ReactNativeGetListener.js | 27 ++++++++++++++----- 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/packages/react-native-renderer/src/ReactFabricHostConfig.js b/packages/react-native-renderer/src/ReactFabricHostConfig.js index 6f0ecf3d9784b..bf6494f7d41c5 100644 --- a/packages/react-native-renderer/src/ReactFabricHostConfig.js +++ b/packages/react-native-renderer/src/ReactFabricHostConfig.js @@ -117,7 +117,7 @@ if (registerEventHandler) { registerEventHandler(dispatchEvent); } -type InternalEventListeners = { [string]: $ReadOnly<{| listener: EventListener, options: EventListenerOptions |}>[] }; +type InternalEventListeners = { [string]: {| listener: EventListener, options: EventListenerOptions, invalidated: boolean |}[] }; /** * This is used for refs on host components. @@ -250,6 +250,7 @@ class ReactFabricHostComponent { namedEventListeners.push({ listener: listener, + invalidated: false, options: { capture: capture, once: once, diff --git a/packages/react-native-renderer/src/ReactNativeGetListener.js b/packages/react-native-renderer/src/ReactNativeGetListener.js index 85314105b1cb9..b6c6dc139cb9b 100644 --- a/packages/react-native-renderer/src/ReactNativeGetListener.js +++ b/packages/react-native-renderer/src/ReactNativeGetListener.js @@ -56,16 +56,29 @@ export default function getListener( continue; } - listeners.push(listenerObj.listener); - - // Only call once? If so, remove from listeners after calling. + // Only call once? + // If so, we ensure that it's only called once by setting a flag + // and by removing it from eventListeners once it is called (but only + // when it's actually been executed). if (listenerObj.options.once) { - toRemove.push(listenerObj); + listeners.push(function () { + var args = Array.prototype.slice.call(arguments); + + // Guard against function being called more than once in + // case there are somehow multiple in-flight references to + // it being processed + if (!listenerObj.invalidated) { + listenerObj.listener.apply(null, args); + listenerObj.invalidated = true; + } + + // Remove from the event listener once it's been called + stateNode.canonical.removeEventListener_unstable(registrationName, listenerObj.listener, listenerObj.capture); + }); + } else { + listeners.push(listenerObj.listener); } } - for (var listenerObj of toRemove) { - stateNode.canonical.removeEventListener_unstable(registrationName, listenerObj.listener, listenerObj.capture); - } } if (listeners.length === 0) { From 29bf6a411d88ae5f0737891b1a3cd1e1531ab8bf Mon Sep 17 00:00:00 2001 From: Joshua Gross Date: Mon, 14 Feb 2022 10:12:20 -0800 Subject: [PATCH 05/27] fix typo, add comments --- .../src/ReactFabricHostConfig.js | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/packages/react-native-renderer/src/ReactFabricHostConfig.js b/packages/react-native-renderer/src/ReactFabricHostConfig.js index bf6494f7d41c5..cb2e69664a841 100644 --- a/packages/react-native-renderer/src/ReactFabricHostConfig.js +++ b/packages/react-native-renderer/src/ReactFabricHostConfig.js @@ -212,15 +212,22 @@ class ReactFabricHostComponent { return; } - dipatchEvent_unstable(event: CustomEvent) { + // This API (dispatchEvent, addEventListener, removeEventListener) attempts to adhere to the + // w3 Level2 Events spec as much as possible, treating HostComponent as a DOM node. + // + // Unless otherwise noted, these methods should "just work" and adhere to the W3 specs. + // If they deviate in a way that is not explicitly noted here, you've found a bug! + // + // See: + // * https://www.w3.org/TR/DOM-Level-2-Events/events.html + // * https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/dispatchEvent + // * https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener + // * https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/removeEventListener + dispatchEvent_unstable(event: CustomEvent) { dispatchEvent(this._internalInstanceHandle, event.type, event); } - // This API adheres to the w3c addEventListener spec. - // See https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener - // See https://www.w3.org/TR/DOM-Level-2-Events/events.html - // - // Exceptions/TODOs: + // Deviations from spec/TODOs: // (1) listener must currently be a function, we do not support EventListener objects yet. // (2) we do not support the `signal` option / AbortSignal yet addEventListener_unstable(eventType: string, listener: EventListener, options: EventListenerOptions | boolean) { From 049a50b738a9c24350c0e74e18efdec5a0ffac48 Mon Sep 17 00:00:00 2001 From: Joshua Gross Date: Mon, 14 Feb 2022 14:32:15 -0800 Subject: [PATCH 06/27] CustomEvent should be typed in react, but implemented in RN repo --- .../react-native-renderer/src/CustomEvent.js | 40 ------------------- .../src/ReactFabricHostConfig.js | 2 +- .../src/ReactNativeTypes.js | 5 +++ .../ReactNativePrivateInterface.js | 3 ++ 4 files changed, 9 insertions(+), 41 deletions(-) delete mode 100644 packages/react-native-renderer/src/CustomEvent.js diff --git a/packages/react-native-renderer/src/CustomEvent.js b/packages/react-native-renderer/src/CustomEvent.js deleted file mode 100644 index 081f2441dd24a..0000000000000 --- a/packages/react-native-renderer/src/CustomEvent.js +++ /dev/null @@ -1,40 +0,0 @@ -/** - * Copyright (c) Meta Platforms, Inc. and affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - * @format - * @flow strict-local - */ - -export type CustomEventOptions = $ReadOnly<{| - bubbles?: boolean, - cancelable?: boolean, - detail?: { ... } -|}>; - -// TODO: should extend an Event base-class that has most of these properties -class CustomEvent extends Event { - type: string; - detail: ?{ ... }; - bubbles: boolean; - cancelable: boolean; - isTrusted: boolean; - - constructor(typeArg: string, options: CustomEventOptions) { - const { bubbles, cancelable } = options; - // TODO: support passing in the `composed` param - super(typeArg, { bubbles, cancelable }); - - this.detail = options.detail; // this would correspond to `NativeEvent` in SyntheticEvent - - // TODO: do we need these since they should be on Event? (for RN we probably need a polyfill) - this.type = typeArg; - this.bubbles = !!(bubbles || false); - this.cancelable = !!(cancelable || false); - this.isTrusted = false; - } -} - -export default CustomEvent; diff --git a/packages/react-native-renderer/src/ReactFabricHostConfig.js b/packages/react-native-renderer/src/ReactFabricHostConfig.js index cb2e69664a841..816dcdff928af 100644 --- a/packages/react-native-renderer/src/ReactFabricHostConfig.js +++ b/packages/react-native-renderer/src/ReactFabricHostConfig.js @@ -10,6 +10,7 @@ import type {ReactNodeList, OffscreenMode} from 'shared/ReactTypes'; import type {ElementRef} from 'react'; import type { + CustomEvent, HostComponent, MeasureInWindowOnSuccessCallback, MeasureLayoutOnSuccessCallback, @@ -23,7 +24,6 @@ import {mountSafeCallback_NOT_REALLY_SAFE} from './NativeMethodsMixinUtils'; import {create, diff} from './ReactNativeAttributePayload'; import {dispatchEvent} from './ReactFabricEventEmitter'; -import CustomEvent from './CustomEvent'; import { DefaultEventPriority, diff --git a/packages/react-native-renderer/src/ReactNativeTypes.js b/packages/react-native-renderer/src/ReactNativeTypes.js index 55150e83517e6..857a40a2ccc4d 100644 --- a/packages/react-native-renderer/src/ReactNativeTypes.js +++ b/packages/react-native-renderer/src/ReactNativeTypes.js @@ -158,6 +158,11 @@ export type TouchedViewDataAtPoint = $ReadOnly<{| ...InspectorData, |}>; +export interface CustomEvent extends Event { + constructor(type: string, eventInitDict?: CustomEvent$Init): void; + detail: any; +} + /** * Flat ReactNative renderer bundles are too big for Flow to parse efficiently. * Provide minimal Flow typing for the high-level RN API and call it a day. diff --git a/packages/react-native-renderer/src/__mocks__/react-native/Libraries/ReactPrivate/ReactNativePrivateInterface.js b/packages/react-native-renderer/src/__mocks__/react-native/Libraries/ReactPrivate/ReactNativePrivateInterface.js index 3eaf3a5a38057..dbfb9910c943e 100644 --- a/packages/react-native-renderer/src/__mocks__/react-native/Libraries/ReactPrivate/ReactNativePrivateInterface.js +++ b/packages/react-native-renderer/src/__mocks__/react-native/Libraries/ReactPrivate/ReactNativePrivateInterface.js @@ -44,4 +44,7 @@ module.exports = { get RawEventEmitter() { return require('./RawEventEmitter').default; }, + get CustomEvent() { + return require('./CustomEvent').default; + }, }; From 88cfd64b74b1337c6d5e68b4a3ebf519863206e2 Mon Sep 17 00:00:00 2001 From: Joshua Gross Date: Thu, 17 Feb 2022 17:41:38 -0800 Subject: [PATCH 07/27] Handle CustomEvent in ReactNativeBridgeEventPlugin --- .../src/ReactNativeBridgeEventPlugin.js | 51 +++++++++++++++---- .../src/ReactNativeGetListener.js | 19 ++++++- packages/shared/ReactVersion.js | 17 +------ scripts/flow/react-native-host-hooks.js | 1 + 4 files changed, 61 insertions(+), 27 deletions(-) diff --git a/packages/react-native-renderer/src/ReactNativeBridgeEventPlugin.js b/packages/react-native-renderer/src/ReactNativeBridgeEventPlugin.js index c10b5233369c5..c24032f3c4a10 100644 --- a/packages/react-native-renderer/src/ReactNativeBridgeEventPlugin.js +++ b/packages/react-native-renderer/src/ReactNativeBridgeEventPlugin.js @@ -13,7 +13,7 @@ import SyntheticEvent from './legacy-events/SyntheticEvent'; import type {PropagationPhases} from './legacy-events/PropagationPhases'; // Module provided by RN: -import {ReactNativeViewConfigRegistry} from 'react-native/Libraries/ReactPrivate/ReactNativePrivateInterface'; +import {CustomEvent, ReactNativeViewConfigRegistry} from 'react-native/Libraries/ReactPrivate/ReactNativePrivateInterface'; import accumulateInto from './legacy-events/accumulateInto'; import getListener from './ReactNativeGetListener'; import forEachAccumulated from './legacy-events/forEachAccumulated'; @@ -27,10 +27,10 @@ const { // Start of inline: the below functions were inlined from // EventPropagator.js, as they deviated from ReactDOM's newer // implementations. -function listenerAtPhase(inst, event, propagationPhase: PropagationPhases) { +function listenerAtPhase(inst, event, propagationPhase: PropagationPhases, isCustomEvent: boolean) { const registrationName = event.dispatchConfig.phasedRegistrationNames[propagationPhase]; - return getListener(inst, registrationName, propagationPhase); + return getListener(inst, registrationName, propagationPhase, isCustomEvent); } function accumulateDirectionalDispatches(inst, phase, event) { @@ -39,7 +39,7 @@ function accumulateDirectionalDispatches(inst, phase, event) { console.error('Dispatching inst must not be null'); } } - const listener = listenerAtPhase(inst, event, phase); + const listener = listenerAtPhase(inst, event, phase, event instanceof CustomEvent); if (listener) { event._dispatchListeners = accumulateInto( event._dispatchListeners, @@ -67,7 +67,7 @@ function getParent(inst) { /** * Simulates the traversal of a two-phase, capture/bubble event dispatch. */ -export function traverseTwoPhase(inst: Object, fn: Function, arg: Function) { +export function traverseTwoPhase(inst: Object, fn: Function, arg: Function, bubbles: boolean) { const path = []; while (inst) { path.push(inst); @@ -79,12 +79,20 @@ export function traverseTwoPhase(inst: Object, fn: Function, arg: Function) { } for (i = 0; i < path.length; i++) { fn(path[i], 'bubbled', arg); + // It's possible this is false for custom events. + if (!bubbles) { + break; + } } } function accumulateTwoPhaseDispatchesSingle(event) { if (event && event.dispatchConfig.phasedRegistrationNames) { - traverseTwoPhase(event._targetInst, accumulateDirectionalDispatches, event); + // bubbles is only set on the dispatchConfig for custom events. + // The `event` param here at this point is a SyntheticEvent, not an Event or CustomEvent. + const bubbles = event.dispatchConfig.isCustomEvent === true ? (!!event.dispatchConfig.bubbles) : true; + + traverseTwoPhase(event._targetInst, accumulateDirectionalDispatches, event, bubbles); } } @@ -106,7 +114,7 @@ function accumulateDispatches( const registrationName = event.dispatchConfig.registrationName; // Since we "do not look for phased registration names", that // should be the same as "bubbled" here, for all intents and purposes...? - const listener = getListener(inst, registrationName, 'bubbled'); + const listener = getListener(inst, registrationName, 'bubbled', !!event.dispatchConfig.isCustomEvent); if (listener) { event._dispatchListeners = accumulateInto( event._dispatchListeners, @@ -149,8 +157,28 @@ const ReactNativeBridgeEventPlugin = { } const bubbleDispatchConfig = customBubblingEventTypes[topLevelType]; const directDispatchConfig = customDirectEventTypes[topLevelType]; + let customEventConfig = null; + if (nativeEvent instanceof CustomEvent) { + // $FlowFixMe + if (topLevelType.indexOf('on') !== 0) { + throw new Error('Custom event name must start with "on"'); + } + nativeEvent.isTrusted = false; + // For now, this custom event name should technically not be used - + // CustomEvents emitted in the system do not result in calling prop handlers. + customEventConfig = { + registrationName: topLevelType, + isCustomEvent: true, + bubbles: nativeEvent.bubbles, + phasedRegistrationNames: { + bubbled: topLevelType, + // $FlowFixMe + captured: topLevelType + 'Capture' + } + } + } - if (!bubbleDispatchConfig && !directDispatchConfig) { + if (!bubbleDispatchConfig && !directDispatchConfig && !customEventConfig) { throw new Error( // $FlowFixMe - Flow doesn't like this string coercion because DOMTopLevelEventType is opaque `Unsupported top level event type "${topLevelType}" dispatched`, @@ -158,12 +186,15 @@ const ReactNativeBridgeEventPlugin = { } const event = SyntheticEvent.getPooled( - bubbleDispatchConfig || directDispatchConfig, + bubbleDispatchConfig || directDispatchConfig || customEventConfig, targetInst, nativeEvent, nativeEventTarget, ); - if (bubbleDispatchConfig) { + if (bubbleDispatchConfig || customEventConfig) { + // All CustomEvents go through two-phase dispatching, even if they + // are non-bubbling events, which is why we put the `bubbles` param + // in accumulateTwoPhaseDispatches(event); } else if (directDispatchConfig) { accumulateDirectDispatches(event); diff --git a/packages/react-native-renderer/src/ReactNativeGetListener.js b/packages/react-native-renderer/src/ReactNativeGetListener.js index b6c6dc139cb9b..baa1afc2cdae1 100644 --- a/packages/react-native-renderer/src/ReactNativeGetListener.js +++ b/packages/react-native-renderer/src/ReactNativeGetListener.js @@ -11,10 +11,27 @@ import type {PropagationPhases} from './legacy-events/PropagationPhases'; import {getFiberCurrentPropsFromNode} from './legacy-events/EventPluginUtils'; +/** + * Get a list of listeners for a specific event, in-order. + * For React Native we treat the props-based function handlers + * as the first-class citizens, and they are always executed first + * for both capture and bubbling phase. + * + * We need "phase" propagated to this point to support the HostComponent + * EventEmitter API, which does not mutate the name of the handler based + * on phase (whereas prop handlers are registered as `onMyEvent` and `onMyEvent_Capture`). + * + * Additionally, we do NOT want CustomEvent events dispatched through + * the EventEmitter directly in JS to be emitted to prop handlers. This + * may change in the future. OTOH, native events emitted into React Native + * will be emitted both to the prop handler function and to imperative event + * listeners. + */ export default function getListener( inst: Fiber, registrationName: string, phase: PropagationPhases, + isCustomEvent: boolean, ): Function | null { // Previously, there was only one possible listener for an event: // the onEventName property in props. @@ -27,7 +44,7 @@ export default function getListener( const stateNode = inst.stateNode; // If null: Work in progress (ex: onload events in incremental mode). - if (stateNode !== null) { + if (stateNode !== null && !isCustomEvent) { const props = getFiberCurrentPropsFromNode(stateNode); if (props === null) { // Work in progress. diff --git a/packages/shared/ReactVersion.js b/packages/shared/ReactVersion.js index 8d357322ca3ac..fbc65449faf8f 100644 --- a/packages/shared/ReactVersion.js +++ b/packages/shared/ReactVersion.js @@ -1,16 +1 @@ -/** - * 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. - */ - -// TODO: this is special because it gets imported during build. -// -// TODO: 17.0.3 has not been released to NPM; -// It exists as a placeholder so that DevTools can support work tag changes between releases. -// When we next publish a release (either 17.0.3 or 17.1.0), update the matching TODO in backend/renderer.js -// TODO: This module is used both by the release scripts and to expose a version -// at runtime. We should instead inject the version number as part of the build -// process, and use the ReactVersions.js module as the single source of truth. -export default '17.0.3'; +export default '18.0.0-rc.0-experimental-049a50b73-20220214'; diff --git a/scripts/flow/react-native-host-hooks.js b/scripts/flow/react-native-host-hooks.js index c7a2fc8dc3212..896964f063643 100644 --- a/scripts/flow/react-native-host-hooks.js +++ b/scripts/flow/react-native-host-hooks.js @@ -138,6 +138,7 @@ declare module 'react-native/Libraries/ReactPrivate/ReactNativePrivateInterface' emit: (channel: string, event: RawEventEmitterEvent) => string, ... }; + declare export var CustomEvent: CustomEvent; } declare module 'react-native/Libraries/ReactPrivate/ReactNativePrivateInitializeCore' { From f79c3fc02ad3c33c5a392ee751f34a876cd5915d Mon Sep 17 00:00:00 2001 From: Joshua Gross Date: Thu, 17 Feb 2022 17:44:21 -0800 Subject: [PATCH 08/27] getEventListener returns an array of functions instead of Function | null --- .../src/ReactNativeGetListener.js | 22 +++---------------- 1 file changed, 3 insertions(+), 19 deletions(-) diff --git a/packages/react-native-renderer/src/ReactNativeGetListener.js b/packages/react-native-renderer/src/ReactNativeGetListener.js index baa1afc2cdae1..ecf0526f35f1b 100644 --- a/packages/react-native-renderer/src/ReactNativeGetListener.js +++ b/packages/react-native-renderer/src/ReactNativeGetListener.js @@ -32,7 +32,7 @@ export default function getListener( registrationName: string, phase: PropagationPhases, isCustomEvent: boolean, -): Function | null { +): Array { // Previously, there was only one possible listener for an event: // the onEventName property in props. // Now, it is also possible to have N listeners @@ -48,7 +48,7 @@ export default function getListener( const props = getFiberCurrentPropsFromNode(stateNode); if (props === null) { // Work in progress. - return null; + return []; } const listener = props[registrationName]; @@ -98,21 +98,5 @@ export default function getListener( } } - if (listeners.length === 0) { - return null; - } - - if (listeners.length === 1) { - return listeners[0]; - } - - // We need to call at least 2 event handlers - return function () { - // any arguments that are passed to the event handlers - var args = Array.prototype.slice.call(arguments); - - for (var i = 0; i < listeners.length; i++) { - listeners[i] && listeners[i].apply(null, args); - } - }; + return listeners; } From 7f2a1b34c34467ef6d0ffd71223755c199f7c20e Mon Sep 17 00:00:00 2001 From: Joshua Gross Date: Wed, 23 Feb 2022 17:15:35 -0800 Subject: [PATCH 09/27] Send data back and forth between Event <> SyntheticEvent in a way that supports the RN Event polyfill --- .../src/ReactNativeBridgeEventPlugin.js | 7 +++++- .../src/legacy-events/SyntheticEvent.js | 25 +++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/packages/react-native-renderer/src/ReactNativeBridgeEventPlugin.js b/packages/react-native-renderer/src/ReactNativeBridgeEventPlugin.js index c24032f3c4a10..1faa5ba535f12 100644 --- a/packages/react-native-renderer/src/ReactNativeBridgeEventPlugin.js +++ b/packages/react-native-renderer/src/ReactNativeBridgeEventPlugin.js @@ -194,9 +194,14 @@ const ReactNativeBridgeEventPlugin = { if (bubbleDispatchConfig || customEventConfig) { // All CustomEvents go through two-phase dispatching, even if they // are non-bubbling events, which is why we put the `bubbles` param - // in + // in the config for CustomEvents only. + // CustomEvents are not emitted to prop handler functions ever. + // Native two-phase events will be emitted to prop handler functions + // and to HostComponent event listeners. accumulateTwoPhaseDispatches(event); } else if (directDispatchConfig) { + // Direct dispatched events do not go to HostComponent EventEmitters, + // they *only* go to the prop function handlers. accumulateDirectDispatches(event); } else { return null; diff --git a/packages/react-native-renderer/src/legacy-events/SyntheticEvent.js b/packages/react-native-renderer/src/legacy-events/SyntheticEvent.js index 84d39bbd603d2..641fbbbc58cad 100644 --- a/packages/react-native-renderer/src/legacy-events/SyntheticEvent.js +++ b/packages/react-native-renderer/src/legacy-events/SyntheticEvent.js @@ -77,6 +77,11 @@ function SyntheticEvent( this._dispatchListeners = null; this._dispatchInstances = null; + // React Native Event polyfill - enable Event proxying calls to SyntheticEvent + if (event.setSyntheticEvent) { + event.setSyntheticEvent(this); + } + const Interface = this.constructor.Interface; for (const propName in Interface) { if (!Interface.hasOwnProperty(propName)) { @@ -118,12 +123,22 @@ Object.assign(SyntheticEvent.prototype, { return; } + // React Native Event polyfill - disable recursion + if (event.setSyntheticEvent) { + event.setSyntheticEvent(null); + } + if (event.preventDefault) { event.preventDefault(); } else if (typeof event.returnValue !== 'unknown') { event.returnValue = false; } this.isDefaultPrevented = functionThatReturnsTrue; + + // React Native Event polyfill - reenable Event proxying calls to SyntheticEvent + if (event.setSyntheticEvent) { + event.setSyntheticEvent(this); + } }, stopPropagation: function() { @@ -132,6 +147,11 @@ Object.assign(SyntheticEvent.prototype, { return; } + // React Native Event polyfill - disable recursion + if (event.setSyntheticEvent) { + event.setSyntheticEvent(null); + } + if (event.stopPropagation) { event.stopPropagation(); } else if (typeof event.cancelBubble !== 'unknown') { @@ -144,6 +164,11 @@ Object.assign(SyntheticEvent.prototype, { } this.isPropagationStopped = functionThatReturnsTrue; + + // React Native Event polyfill - reenable Event proxying calls to SyntheticEvent + if (event.setSyntheticEvent) { + event.setSyntheticEvent(this); + } }, /** From 7f3a82cab21450e20f89c8761580f5b8e9e1c95c Mon Sep 17 00:00:00 2001 From: Joshua Gross Date: Wed, 23 Feb 2022 19:16:35 -0800 Subject: [PATCH 10/27] typo --- .../react-native-renderer/src/legacy-events/SyntheticEvent.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/react-native-renderer/src/legacy-events/SyntheticEvent.js b/packages/react-native-renderer/src/legacy-events/SyntheticEvent.js index 641fbbbc58cad..1d1d7a3b93bb7 100644 --- a/packages/react-native-renderer/src/legacy-events/SyntheticEvent.js +++ b/packages/react-native-renderer/src/legacy-events/SyntheticEvent.js @@ -78,8 +78,8 @@ function SyntheticEvent( this._dispatchInstances = null; // React Native Event polyfill - enable Event proxying calls to SyntheticEvent - if (event.setSyntheticEvent) { - event.setSyntheticEvent(this); + if (nativeEvent.setSyntheticEvent) { + nativeEvent.setSyntheticEvent(this); } const Interface = this.constructor.Interface; From 5a8047ddbc91ea554a12dbb565d8187b590599fd Mon Sep 17 00:00:00 2001 From: Joshua Gross Date: Wed, 23 Feb 2022 20:54:04 -0800 Subject: [PATCH 11/27] fix null deref --- .../react-native-renderer/src/ReactNativeGetListener.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/react-native-renderer/src/ReactNativeGetListener.js b/packages/react-native-renderer/src/ReactNativeGetListener.js index ecf0526f35f1b..11fdd0680739f 100644 --- a/packages/react-native-renderer/src/ReactNativeGetListener.js +++ b/packages/react-native-renderer/src/ReactNativeGetListener.js @@ -43,8 +43,13 @@ export default function getListener( const listeners = []; const stateNode = inst.stateNode; + + if (stateNode === null) { + return listeners; + } + // If null: Work in progress (ex: onload events in incremental mode). - if (stateNode !== null && !isCustomEvent) { + if (!isCustomEvent) { const props = getFiberCurrentPropsFromNode(stateNode); if (props === null) { // Work in progress. From 3f38d4a212d28a5ff22afece80d6dd02727ea6f4 Mon Sep 17 00:00:00 2001 From: Joshua Gross Date: Thu, 24 Feb 2022 00:24:47 -0800 Subject: [PATCH 12/27] fix dispatch / listener code to accept 0 or more listeners and handle 2+ listeners --- .../src/ReactNativeBridgeEventPlugin.js | 12 ++++++++---- .../src/ReactNativeGetListener.js | 10 +++++++--- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/packages/react-native-renderer/src/ReactNativeBridgeEventPlugin.js b/packages/react-native-renderer/src/ReactNativeBridgeEventPlugin.js index 1faa5ba535f12..cf1699665bc1f 100644 --- a/packages/react-native-renderer/src/ReactNativeBridgeEventPlugin.js +++ b/packages/react-native-renderer/src/ReactNativeBridgeEventPlugin.js @@ -114,13 +114,17 @@ function accumulateDispatches( const registrationName = event.dispatchConfig.registrationName; // Since we "do not look for phased registration names", that // should be the same as "bubbled" here, for all intents and purposes...? - const listener = getListener(inst, registrationName, 'bubbled', !!event.dispatchConfig.isCustomEvent); - if (listener) { + const listeners = getListener(inst, registrationName, 'bubbled', !!event.dispatchConfig.isCustomEvent); + if (listeners) { event._dispatchListeners = accumulateInto( event._dispatchListeners, - listener, + listeners, ); - event._dispatchInstances = accumulateInto(event._dispatchInstances, inst); + // we need an inst for every listener + var insts = listeners.map(() => { + return inst; + }); + event._dispatchInstances = accumulateInto(event._dispatchInstances, insts); } } } diff --git a/packages/react-native-renderer/src/ReactNativeGetListener.js b/packages/react-native-renderer/src/ReactNativeGetListener.js index 11fdd0680739f..f550c3c2543c9 100644 --- a/packages/react-native-renderer/src/ReactNativeGetListener.js +++ b/packages/react-native-renderer/src/ReactNativeGetListener.js @@ -32,7 +32,7 @@ export default function getListener( registrationName: string, phase: PropagationPhases, isCustomEvent: boolean, -): Array { +): Array | null { // Previously, there was only one possible listener for an event: // the onEventName property in props. // Now, it is also possible to have N listeners @@ -45,7 +45,7 @@ export default function getListener( const stateNode = inst.stateNode; if (stateNode === null) { - return listeners; + return null; } // If null: Work in progress (ex: onload events in incremental mode). @@ -53,7 +53,7 @@ export default function getListener( const props = getFiberCurrentPropsFromNode(stateNode); if (props === null) { // Work in progress. - return []; + return null; } const listener = props[registrationName]; @@ -103,5 +103,9 @@ export default function getListener( } } + if (listeners.length === 0) { + return null; + } + return listeners; } From 1055e5634b4f21af27ada5a00907762c7327d235 Mon Sep 17 00:00:00 2001 From: Joshua Gross Date: Thu, 24 Feb 2022 09:53:50 -0800 Subject: [PATCH 13/27] fix another site where insts.length needs to == listeners.length --- .../src/ReactNativeBridgeEventPlugin.js | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/react-native-renderer/src/ReactNativeBridgeEventPlugin.js b/packages/react-native-renderer/src/ReactNativeBridgeEventPlugin.js index cf1699665bc1f..4511f6aa93ffa 100644 --- a/packages/react-native-renderer/src/ReactNativeBridgeEventPlugin.js +++ b/packages/react-native-renderer/src/ReactNativeBridgeEventPlugin.js @@ -39,13 +39,14 @@ function accumulateDirectionalDispatches(inst, phase, event) { console.error('Dispatching inst must not be null'); } } - const listener = listenerAtPhase(inst, event, phase, event instanceof CustomEvent); - if (listener) { + const listeners = listenerAtPhase(inst, event, phase, event instanceof CustomEvent); + if (listeners && listeners.length > 0) { event._dispatchListeners = accumulateInto( event._dispatchListeners, - listener, + listeners, ); - event._dispatchInstances = accumulateInto(event._dispatchInstances, inst); + const insts = listeners.map(() => { return inst; }); + event._dispatchInstances = accumulateInto(event._dispatchInstances, insts); } } @@ -120,7 +121,7 @@ function accumulateDispatches( event._dispatchListeners, listeners, ); - // we need an inst for every listener + // an inst for every listener var insts = listeners.map(() => { return inst; }); From f1746d28eccb16571d19d617d3dbbd71cce62438 Mon Sep 17 00:00:00 2001 From: Joshua Gross Date: Thu, 24 Feb 2022 09:55:17 -0800 Subject: [PATCH 14/27] getListener -> getListeners --- .../src/ReactNativeBridgeEventPlugin.js | 10 +- .../src/ReactNativeGetListener.js | 111 ------------------ 2 files changed, 5 insertions(+), 116 deletions(-) delete mode 100644 packages/react-native-renderer/src/ReactNativeGetListener.js diff --git a/packages/react-native-renderer/src/ReactNativeBridgeEventPlugin.js b/packages/react-native-renderer/src/ReactNativeBridgeEventPlugin.js index 4511f6aa93ffa..8f093d1bad502 100644 --- a/packages/react-native-renderer/src/ReactNativeBridgeEventPlugin.js +++ b/packages/react-native-renderer/src/ReactNativeBridgeEventPlugin.js @@ -15,7 +15,7 @@ import type {PropagationPhases} from './legacy-events/PropagationPhases'; // Module provided by RN: import {CustomEvent, ReactNativeViewConfigRegistry} from 'react-native/Libraries/ReactPrivate/ReactNativePrivateInterface'; import accumulateInto from './legacy-events/accumulateInto'; -import getListener from './ReactNativeGetListener'; +import getListeners from './ReactNativeGetListeners'; import forEachAccumulated from './legacy-events/forEachAccumulated'; import {HostComponent} from 'react-reconciler/src/ReactWorkTags'; @@ -27,10 +27,10 @@ const { // Start of inline: the below functions were inlined from // EventPropagator.js, as they deviated from ReactDOM's newer // implementations. -function listenerAtPhase(inst, event, propagationPhase: PropagationPhases, isCustomEvent: boolean) { +function listenersAtPhase(inst, event, propagationPhase: PropagationPhases, isCustomEvent: boolean) { const registrationName = event.dispatchConfig.phasedRegistrationNames[propagationPhase]; - return getListener(inst, registrationName, propagationPhase, isCustomEvent); + return getListeners(inst, registrationName, propagationPhase, isCustomEvent); } function accumulateDirectionalDispatches(inst, phase, event) { @@ -39,7 +39,7 @@ function accumulateDirectionalDispatches(inst, phase, event) { console.error('Dispatching inst must not be null'); } } - const listeners = listenerAtPhase(inst, event, phase, event instanceof CustomEvent); + const listeners = listenersAtPhase(inst, event, phase, event instanceof CustomEvent); if (listeners && listeners.length > 0) { event._dispatchListeners = accumulateInto( event._dispatchListeners, @@ -115,7 +115,7 @@ function accumulateDispatches( const registrationName = event.dispatchConfig.registrationName; // Since we "do not look for phased registration names", that // should be the same as "bubbled" here, for all intents and purposes...? - const listeners = getListener(inst, registrationName, 'bubbled', !!event.dispatchConfig.isCustomEvent); + const listeners = getListeners(inst, registrationName, 'bubbled', !!event.dispatchConfig.isCustomEvent); if (listeners) { event._dispatchListeners = accumulateInto( event._dispatchListeners, diff --git a/packages/react-native-renderer/src/ReactNativeGetListener.js b/packages/react-native-renderer/src/ReactNativeGetListener.js deleted file mode 100644 index f550c3c2543c9..0000000000000 --- a/packages/react-native-renderer/src/ReactNativeGetListener.js +++ /dev/null @@ -1,111 +0,0 @@ -/** - * 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 - */ - -import type {Fiber} from 'react-reconciler/src/ReactInternalTypes'; -import type {PropagationPhases} from './legacy-events/PropagationPhases'; - -import {getFiberCurrentPropsFromNode} from './legacy-events/EventPluginUtils'; - -/** - * Get a list of listeners for a specific event, in-order. - * For React Native we treat the props-based function handlers - * as the first-class citizens, and they are always executed first - * for both capture and bubbling phase. - * - * We need "phase" propagated to this point to support the HostComponent - * EventEmitter API, which does not mutate the name of the handler based - * on phase (whereas prop handlers are registered as `onMyEvent` and `onMyEvent_Capture`). - * - * Additionally, we do NOT want CustomEvent events dispatched through - * the EventEmitter directly in JS to be emitted to prop handlers. This - * may change in the future. OTOH, native events emitted into React Native - * will be emitted both to the prop handler function and to imperative event - * listeners. - */ -export default function getListener( - inst: Fiber, - registrationName: string, - phase: PropagationPhases, - isCustomEvent: boolean, -): Array | null { - // Previously, there was only one possible listener for an event: - // the onEventName property in props. - // Now, it is also possible to have N listeners - // for a specific event on a node. Thus, we accumulate all of the listeners, - // including the props listener, and return a function that calls them all in - // order, starting with the handler prop and then the listeners in order. - // We still return a single function or null. - const listeners = []; - - const stateNode = inst.stateNode; - - if (stateNode === null) { - return null; - } - - // If null: Work in progress (ex: onload events in incremental mode). - if (!isCustomEvent) { - const props = getFiberCurrentPropsFromNode(stateNode); - if (props === null) { - // Work in progress. - return null; - } - const listener = props[registrationName]; - - if (listener && typeof listener !== 'function') { - throw new Error( - `Expected \`${registrationName}\` listener to be a function, instead got a value of \`${typeof listener}\` type.`, - ); - } - - if (listener) { - listeners.push(listener); - } - } - - // Get imperative event listeners for this event - if (stateNode.canonical && stateNode.canonical._eventListeners && stateNode.canonical._eventListeners[registrationName] && stateNode.canonical._eventListeners[registrationName].length > 0) { - var toRemove = []; - for (var listenerObj of stateNode.canonical._eventListeners[registrationName]) { - // Make sure phase of listener matches requested phase - const isCaptureEvent = listenerObj.options.capture != null && listenerObj.options.capture; - if (isCaptureEvent !== (phase === 'captured')) { - continue; - } - - // Only call once? - // If so, we ensure that it's only called once by setting a flag - // and by removing it from eventListeners once it is called (but only - // when it's actually been executed). - if (listenerObj.options.once) { - listeners.push(function () { - var args = Array.prototype.slice.call(arguments); - - // Guard against function being called more than once in - // case there are somehow multiple in-flight references to - // it being processed - if (!listenerObj.invalidated) { - listenerObj.listener.apply(null, args); - listenerObj.invalidated = true; - } - - // Remove from the event listener once it's been called - stateNode.canonical.removeEventListener_unstable(registrationName, listenerObj.listener, listenerObj.capture); - }); - } else { - listeners.push(listenerObj.listener); - } - } - } - - if (listeners.length === 0) { - return null; - } - - return listeners; -} From df4a1fd5fe708d26c3d61ba6331e662ee46fae8a Mon Sep 17 00:00:00 2001 From: Joshua Gross Date: Thu, 24 Feb 2022 10:02:55 -0800 Subject: [PATCH 15/27] fix flow --- packages/react-native-renderer/src/ReactFabricEventEmitter.js | 4 ++-- packages/react-native-renderer/src/ReactNativeEventEmitter.js | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/react-native-renderer/src/ReactFabricEventEmitter.js b/packages/react-native-renderer/src/ReactFabricEventEmitter.js index d8492156fdc7c..c3b89e91779a3 100644 --- a/packages/react-native-renderer/src/ReactFabricEventEmitter.js +++ b/packages/react-native-renderer/src/ReactFabricEventEmitter.js @@ -18,12 +18,12 @@ import {batchedUpdates} from './legacy-events/ReactGenericBatching'; import accumulateInto from './legacy-events/accumulateInto'; import {plugins} from './legacy-events/EventPluginRegistry'; -import getListener from './ReactNativeGetListener'; +import getListeners from './ReactNativeGetListeners'; import {runEventsInBatch} from './legacy-events/EventBatching'; import {RawEventEmitter} from 'react-native/Libraries/ReactPrivate/ReactNativePrivateInterface'; -export {getListener, registrationNameModules as registrationNames}; +export {getListeners, registrationNameModules as registrationNames}; /** * Allows registered plugins an opportunity to extract events from top-level diff --git a/packages/react-native-renderer/src/ReactNativeEventEmitter.js b/packages/react-native-renderer/src/ReactNativeEventEmitter.js index 2ba35aed39b9f..91816a53f82da 100644 --- a/packages/react-native-renderer/src/ReactNativeEventEmitter.js +++ b/packages/react-native-renderer/src/ReactNativeEventEmitter.js @@ -17,12 +17,12 @@ import {registrationNameModules} from './legacy-events/EventPluginRegistry'; import {batchedUpdates} from './legacy-events/ReactGenericBatching'; import {runEventsInBatch} from './legacy-events/EventBatching'; import {plugins} from './legacy-events/EventPluginRegistry'; -import getListener from './ReactNativeGetListener'; +import getListeners from './ReactNativeGetListeners'; import accumulateInto from './legacy-events/accumulateInto'; import {getInstanceFromNode} from './ReactNativeComponentTree'; -export {getListener, registrationNameModules as registrationNames}; +export {getListeners, registrationNameModules as registrationNames}; /** * Version of `ReactBrowserEventEmitter` that works on the receiving side of a From e5f519908330e66beef43f14c7bb8081f7048f7c Mon Sep 17 00:00:00 2001 From: Joshua Gross Date: Thu, 24 Feb 2022 10:30:44 -0800 Subject: [PATCH 16/27] missing files --- .../src/ReactNativeGetListeners.js | 111 ++++++++++++++++++ .../Libraries/ReactPrivate/CustomEvent.js | 0 2 files changed, 111 insertions(+) create mode 100644 packages/react-native-renderer/src/ReactNativeGetListeners.js create mode 100644 packages/react-native-renderer/src/__mocks__/react-native/Libraries/ReactPrivate/CustomEvent.js diff --git a/packages/react-native-renderer/src/ReactNativeGetListeners.js b/packages/react-native-renderer/src/ReactNativeGetListeners.js new file mode 100644 index 0000000000000..4d1fa13887fed --- /dev/null +++ b/packages/react-native-renderer/src/ReactNativeGetListeners.js @@ -0,0 +1,111 @@ +/** + * 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 + */ + +import type {Fiber} from 'react-reconciler/src/ReactInternalTypes'; +import type {PropagationPhases} from './legacy-events/PropagationPhases'; + +import {getFiberCurrentPropsFromNode} from './legacy-events/EventPluginUtils'; + +/** + * Get a list of listeners for a specific event, in-order. + * For React Native we treat the props-based function handlers + * as the first-class citizens, and they are always executed first + * for both capture and bubbling phase. + * + * We need "phase" propagated to this point to support the HostComponent + * EventEmitter API, which does not mutate the name of the handler based + * on phase (whereas prop handlers are registered as `onMyEvent` and `onMyEvent_Capture`). + * + * Additionally, we do NOT want CustomEvent events dispatched through + * the EventEmitter directly in JS to be emitted to prop handlers. This + * may change in the future. OTOH, native events emitted into React Native + * will be emitted both to the prop handler function and to imperative event + * listeners. + */ +export default function getListeners( + inst: Fiber, + registrationName: string, + phase: PropagationPhases, + isCustomEvent: boolean, +): Array | null { + // Previously, there was only one possible listener for an event: + // the onEventName property in props. + // Now, it is also possible to have N listeners + // for a specific event on a node. Thus, we accumulate all of the listeners, + // including the props listener, and return a function that calls them all in + // order, starting with the handler prop and then the listeners in order. + // We still return a single function or null. + const listeners = []; + + const stateNode = inst.stateNode; + + if (stateNode === null) { + return null; + } + + // If null: Work in progress (ex: onload events in incremental mode). + if (!isCustomEvent) { + const props = getFiberCurrentPropsFromNode(stateNode); + if (props === null) { + // Work in progress. + return null; + } + const listener = props[registrationName]; + + if (listener && typeof listener !== 'function') { + throw new Error( + `Expected \`${registrationName}\` listener to be a function, instead got a value of \`${typeof listener}\` type.`, + ); + } + + if (listener) { + listeners.push(listener); + } + } + + // Get imperative event listeners for this event + if (stateNode.canonical && stateNode.canonical._eventListeners && stateNode.canonical._eventListeners[registrationName] && stateNode.canonical._eventListeners[registrationName].length > 0) { + var toRemove = []; + for (var listenerObj of stateNode.canonical._eventListeners[registrationName]) { + // Make sure phase of listener matches requested phase + const isCaptureEvent = listenerObj.options.capture != null && listenerObj.options.capture; + if (isCaptureEvent !== (phase === 'captured')) { + continue; + } + + // Only call once? + // If so, we ensure that it's only called once by setting a flag + // and by removing it from eventListeners once it is called (but only + // when it's actually been executed). + if (listenerObj.options.once) { + listeners.push(function () { + var args = Array.prototype.slice.call(arguments); + + // Guard against function being called more than once in + // case there are somehow multiple in-flight references to + // it being processed + if (!listenerObj.invalidated) { + listenerObj.listener.apply(null, args); + listenerObj.invalidated = true; + } + + // Remove from the event listener once it's been called + stateNode.canonical.removeEventListener_unstable(registrationName, listenerObj.listener, listenerObj.capture); + }); + } else { + listeners.push(listenerObj.listener); + } + } + } + + if (listeners.length === 0) { + return null; + } + + return listeners; +} diff --git a/packages/react-native-renderer/src/__mocks__/react-native/Libraries/ReactPrivate/CustomEvent.js b/packages/react-native-renderer/src/__mocks__/react-native/Libraries/ReactPrivate/CustomEvent.js new file mode 100644 index 0000000000000..e69de29bb2d1d From 901742a7fde9739a6d30be71dfc066f07846a823 Mon Sep 17 00:00:00 2001 From: Joshua Gross Date: Thu, 24 Feb 2022 10:38:53 -0800 Subject: [PATCH 17/27] prettier --- .../src/ReactFabricHostConfig.js | 43 +++++++++---- .../src/ReactNativeBridgeEventPlugin.js | 64 +++++++++++++++---- .../src/ReactNativeGetListeners.js | 22 +++++-- 3 files changed, 99 insertions(+), 30 deletions(-) diff --git a/packages/react-native-renderer/src/ReactFabricHostConfig.js b/packages/react-native-renderer/src/ReactFabricHostConfig.js index 816dcdff928af..af9b9926e083f 100644 --- a/packages/react-native-renderer/src/ReactFabricHostConfig.js +++ b/packages/react-native-renderer/src/ReactFabricHostConfig.js @@ -117,7 +117,13 @@ if (registerEventHandler) { registerEventHandler(dispatchEvent); } -type InternalEventListeners = { [string]: {| listener: EventListener, options: EventListenerOptions, invalidated: boolean |}[] }; +type InternalEventListeners = { + [string]: {| + listener: EventListener, + options: EventListenerOptions, + invalidated: boolean, + |}[], +}; /** * This is used for refs on host components. @@ -230,7 +236,11 @@ class ReactFabricHostComponent { // Deviations from spec/TODOs: // (1) listener must currently be a function, we do not support EventListener objects yet. // (2) we do not support the `signal` option / AbortSignal yet - addEventListener_unstable(eventType: string, listener: EventListener, options: EventListenerOptions | boolean) { + addEventListener_unstable( + eventType: string, + listener: EventListener, + options: EventListenerOptions | boolean, + ) { if (typeof eventType !== 'string') { throw new Error('addEventListener_unstable eventType must be a string'); } @@ -239,8 +249,10 @@ class ReactFabricHostComponent { } // The third argument is either boolean indicating "captures" or an object. - const optionsObj = (typeof options === 'object' && options !== null ? options : {}); - const capture = (typeof options === 'boolean' ? options : (optionsObj.capture)) || false; + const optionsObj = + typeof options === 'object' && options !== null ? options : {}; + const capture = + (typeof options === 'boolean' ? options : optionsObj.capture) || false; const once = optionsObj.once || false; const passive = optionsObj.passive || false; const signal = null; // TODO: implement signal/AbortSignal @@ -262,19 +274,25 @@ class ReactFabricHostComponent { capture: capture, once: once, passive: passive, - signal: signal - } + signal: signal, + }, }); } // See https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/removeEventListener - removeEventListener_unstable(eventType: string, listener: EventListener, options: EventListenerRemoveOptions | boolean) { + removeEventListener_unstable( + eventType: string, + listener: EventListener, + options: EventListenerRemoveOptions | boolean, + ) { // eventType and listener must be referentially equal to be removed from the listeners // data structure, but in "options" we only check the `capture` flag, according to spec. // That means if you add the same function as a listener with capture set to true and false, // you must also call removeEventListener twice with capture set to true/false. - const optionsObj = (typeof options === 'object' && options !== null ? options : {}); - const capture = (typeof options === 'boolean' ? options : (optionsObj.capture)) || false; + const optionsObj = + typeof options === 'object' && options !== null ? options : {}; + const capture = + (typeof options === 'boolean' ? options : optionsObj.capture) || false; // If there are no event listeners or named event listeners, we can bail early - our // job is already done. @@ -288,9 +306,12 @@ class ReactFabricHostComponent { } eventListeners[eventType] = namedEventListeners.filter(listenerObj => { - return !(listenerObj.listener === listener && listenerObj.options.capture === capture); + return !( + listenerObj.listener === listener && + listenerObj.options.capture === capture + ); }); - }; + } } // eslint-disable-next-line no-unused-expressions diff --git a/packages/react-native-renderer/src/ReactNativeBridgeEventPlugin.js b/packages/react-native-renderer/src/ReactNativeBridgeEventPlugin.js index 8f093d1bad502..d0d9ab0089717 100644 --- a/packages/react-native-renderer/src/ReactNativeBridgeEventPlugin.js +++ b/packages/react-native-renderer/src/ReactNativeBridgeEventPlugin.js @@ -13,7 +13,10 @@ import SyntheticEvent from './legacy-events/SyntheticEvent'; import type {PropagationPhases} from './legacy-events/PropagationPhases'; // Module provided by RN: -import {CustomEvent, ReactNativeViewConfigRegistry} from 'react-native/Libraries/ReactPrivate/ReactNativePrivateInterface'; +import { + CustomEvent, + ReactNativeViewConfigRegistry, +} from 'react-native/Libraries/ReactPrivate/ReactNativePrivateInterface'; import accumulateInto from './legacy-events/accumulateInto'; import getListeners from './ReactNativeGetListeners'; import forEachAccumulated from './legacy-events/forEachAccumulated'; @@ -27,7 +30,12 @@ const { // Start of inline: the below functions were inlined from // EventPropagator.js, as they deviated from ReactDOM's newer // implementations. -function listenersAtPhase(inst, event, propagationPhase: PropagationPhases, isCustomEvent: boolean) { +function listenersAtPhase( + inst, + event, + propagationPhase: PropagationPhases, + isCustomEvent: boolean, +) { const registrationName = event.dispatchConfig.phasedRegistrationNames[propagationPhase]; return getListeners(inst, registrationName, propagationPhase, isCustomEvent); @@ -39,13 +47,20 @@ function accumulateDirectionalDispatches(inst, phase, event) { console.error('Dispatching inst must not be null'); } } - const listeners = listenersAtPhase(inst, event, phase, event instanceof CustomEvent); + const listeners = listenersAtPhase( + inst, + event, + phase, + event instanceof CustomEvent, + ); if (listeners && listeners.length > 0) { event._dispatchListeners = accumulateInto( event._dispatchListeners, listeners, ); - const insts = listeners.map(() => { return inst; }); + const insts = listeners.map(() => { + return inst; + }); event._dispatchInstances = accumulateInto(event._dispatchInstances, insts); } } @@ -68,7 +83,12 @@ function getParent(inst) { /** * Simulates the traversal of a two-phase, capture/bubble event dispatch. */ -export function traverseTwoPhase(inst: Object, fn: Function, arg: Function, bubbles: boolean) { +export function traverseTwoPhase( + inst: Object, + fn: Function, + arg: Function, + bubbles: boolean, +) { const path = []; while (inst) { path.push(inst); @@ -91,9 +111,17 @@ function accumulateTwoPhaseDispatchesSingle(event) { if (event && event.dispatchConfig.phasedRegistrationNames) { // bubbles is only set on the dispatchConfig for custom events. // The `event` param here at this point is a SyntheticEvent, not an Event or CustomEvent. - const bubbles = event.dispatchConfig.isCustomEvent === true ? (!!event.dispatchConfig.bubbles) : true; - - traverseTwoPhase(event._targetInst, accumulateDirectionalDispatches, event, bubbles); + const bubbles = + event.dispatchConfig.isCustomEvent === true + ? !!event.dispatchConfig.bubbles + : true; + + traverseTwoPhase( + event._targetInst, + accumulateDirectionalDispatches, + event, + bubbles, + ); } } @@ -115,7 +143,12 @@ function accumulateDispatches( const registrationName = event.dispatchConfig.registrationName; // Since we "do not look for phased registration names", that // should be the same as "bubbled" here, for all intents and purposes...? - const listeners = getListeners(inst, registrationName, 'bubbled', !!event.dispatchConfig.isCustomEvent); + const listeners = getListeners( + inst, + registrationName, + 'bubbled', + !!event.dispatchConfig.isCustomEvent, + ); if (listeners) { event._dispatchListeners = accumulateInto( event._dispatchListeners, @@ -123,9 +156,12 @@ function accumulateDispatches( ); // an inst for every listener var insts = listeners.map(() => { - return inst; + return inst; }); - event._dispatchInstances = accumulateInto(event._dispatchInstances, insts); + event._dispatchInstances = accumulateInto( + event._dispatchInstances, + insts, + ); } } } @@ -178,9 +214,9 @@ const ReactNativeBridgeEventPlugin = { phasedRegistrationNames: { bubbled: topLevelType, // $FlowFixMe - captured: topLevelType + 'Capture' - } - } + captured: topLevelType + 'Capture', + }, + }; } if (!bubbleDispatchConfig && !directDispatchConfig && !customEventConfig) { diff --git a/packages/react-native-renderer/src/ReactNativeGetListeners.js b/packages/react-native-renderer/src/ReactNativeGetListeners.js index 4d1fa13887fed..09a9973abea37 100644 --- a/packages/react-native-renderer/src/ReactNativeGetListeners.js +++ b/packages/react-native-renderer/src/ReactNativeGetListeners.js @@ -69,11 +69,19 @@ export default function getListeners( } // Get imperative event listeners for this event - if (stateNode.canonical && stateNode.canonical._eventListeners && stateNode.canonical._eventListeners[registrationName] && stateNode.canonical._eventListeners[registrationName].length > 0) { + if ( + stateNode.canonical && + stateNode.canonical._eventListeners && + stateNode.canonical._eventListeners[registrationName] && + stateNode.canonical._eventListeners[registrationName].length > 0 + ) { var toRemove = []; - for (var listenerObj of stateNode.canonical._eventListeners[registrationName]) { + for (var listenerObj of stateNode.canonical._eventListeners[ + registrationName + ]) { // Make sure phase of listener matches requested phase - const isCaptureEvent = listenerObj.options.capture != null && listenerObj.options.capture; + const isCaptureEvent = + listenerObj.options.capture != null && listenerObj.options.capture; if (isCaptureEvent !== (phase === 'captured')) { continue; } @@ -83,7 +91,7 @@ export default function getListeners( // and by removing it from eventListeners once it is called (but only // when it's actually been executed). if (listenerObj.options.once) { - listeners.push(function () { + listeners.push(function() { var args = Array.prototype.slice.call(arguments); // Guard against function being called more than once in @@ -95,7 +103,11 @@ export default function getListeners( } // Remove from the event listener once it's been called - stateNode.canonical.removeEventListener_unstable(registrationName, listenerObj.listener, listenerObj.capture); + stateNode.canonical.removeEventListener_unstable( + registrationName, + listenerObj.listener, + listenerObj.capture, + ); }); } else { listeners.push(listenerObj.listener); From a78b09889ed3d5803924bf9325bd4981129e2d59 Mon Sep 17 00:00:00 2001 From: Joshua Gross Date: Thu, 24 Feb 2022 11:28:54 -0800 Subject: [PATCH 18/27] fix lints, clean up --- .../src/ReactNativeBridgeEventPlugin.js | 2 +- .../src/ReactNativeGetListeners.js | 14 ++++++++------ 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/packages/react-native-renderer/src/ReactNativeBridgeEventPlugin.js b/packages/react-native-renderer/src/ReactNativeBridgeEventPlugin.js index d0d9ab0089717..8f0b3af83549f 100644 --- a/packages/react-native-renderer/src/ReactNativeBridgeEventPlugin.js +++ b/packages/react-native-renderer/src/ReactNativeBridgeEventPlugin.js @@ -155,7 +155,7 @@ function accumulateDispatches( listeners, ); // an inst for every listener - var insts = listeners.map(() => { + const insts = listeners.map(() => { return inst; }); event._dispatchInstances = accumulateInto( diff --git a/packages/react-native-renderer/src/ReactNativeGetListeners.js b/packages/react-native-renderer/src/ReactNativeGetListeners.js index 09a9973abea37..76ba06376ed9e 100644 --- a/packages/react-native-renderer/src/ReactNativeGetListeners.js +++ b/packages/react-native-renderer/src/ReactNativeGetListeners.js @@ -75,14 +75,16 @@ export default function getListeners( stateNode.canonical._eventListeners[registrationName] && stateNode.canonical._eventListeners[registrationName].length > 0 ) { - var toRemove = []; - for (var listenerObj of stateNode.canonical._eventListeners[ - registrationName - ]) { + const eventListeners = + stateNode.canonical._eventListeners[registrationName]; + const requestedPhaseIsCapture = phase === 'captured'; + for (let i = 0; i < eventListeners.length; i++) { + const listenerObj = eventListeners[i]; + // Make sure phase of listener matches requested phase const isCaptureEvent = listenerObj.options.capture != null && listenerObj.options.capture; - if (isCaptureEvent !== (phase === 'captured')) { + if (isCaptureEvent !== requestedPhaseIsCapture) { continue; } @@ -92,7 +94,7 @@ export default function getListeners( // when it's actually been executed). if (listenerObj.options.once) { listeners.push(function() { - var args = Array.prototype.slice.call(arguments); + const args = Array.prototype.slice.call(arguments); // Guard against function being called more than once in // case there are somehow multiple in-flight references to From 633f9eb29a9a85a3b7696779a36bd670854abe9e Mon Sep 17 00:00:00 2001 From: Joshua Gross Date: Thu, 24 Feb 2022 11:31:28 -0800 Subject: [PATCH 19/27] initialize listeners to null, only create empty array if there is a listener to add to it - to avoid creating empty arrays that aren't ever used in a hot path --- .../src/ReactNativeGetListeners.js | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/packages/react-native-renderer/src/ReactNativeGetListeners.js b/packages/react-native-renderer/src/ReactNativeGetListeners.js index 76ba06376ed9e..96a8fdb3af233 100644 --- a/packages/react-native-renderer/src/ReactNativeGetListeners.js +++ b/packages/react-native-renderer/src/ReactNativeGetListeners.js @@ -39,8 +39,8 @@ export default function getListeners( // for a specific event on a node. Thus, we accumulate all of the listeners, // including the props listener, and return a function that calls them all in // order, starting with the handler prop and then the listeners in order. - // We still return a single function or null. - const listeners = []; + // We return either a non-empty array or null. + let listeners = null; const stateNode = inst.stateNode; @@ -64,6 +64,9 @@ export default function getListeners( } if (listener) { + if (listeners === null) { + listeners = []; + } listeners.push(listener); } } @@ -112,14 +115,13 @@ export default function getListeners( ); }); } else { + if (listeners === null) { + listeners = []; + } listeners.push(listenerObj.listener); } } } - if (listeners.length === 0) { - return null; - } - return listeners; } From 7adec888c2a80b7956dd6e2fc96650bf576a7afd Mon Sep 17 00:00:00 2001 From: Joshua Gross Date: Thu, 24 Feb 2022 11:37:02 -0800 Subject: [PATCH 20/27] initialize listeners to null, only create empty array if there is a listener to add to it - to avoid creating empty arrays that aren't ever used in a hot path --- .../react-native-renderer/src/ReactNativeGetListeners.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/react-native-renderer/src/ReactNativeGetListeners.js b/packages/react-native-renderer/src/ReactNativeGetListeners.js index 96a8fdb3af233..f61ec481d932a 100644 --- a/packages/react-native-renderer/src/ReactNativeGetListeners.js +++ b/packages/react-native-renderer/src/ReactNativeGetListeners.js @@ -95,6 +95,9 @@ export default function getListeners( // If so, we ensure that it's only called once by setting a flag // and by removing it from eventListeners once it is called (but only // when it's actually been executed). + if (listeners === null) { + listeners = []; + } if (listenerObj.options.once) { listeners.push(function() { const args = Array.prototype.slice.call(arguments); @@ -115,9 +118,6 @@ export default function getListeners( ); }); } else { - if (listeners === null) { - listeners = []; - } listeners.push(listenerObj.listener); } } From 5bc7103f9400886a9ae58f55c66d70c1e5bd65ea Mon Sep 17 00:00:00 2001 From: Joshua Gross Date: Thu, 24 Feb 2022 11:44:33 -0800 Subject: [PATCH 21/27] var in for loop to make ci happy? --- packages/react-native-renderer/src/ReactNativeGetListeners.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-native-renderer/src/ReactNativeGetListeners.js b/packages/react-native-renderer/src/ReactNativeGetListeners.js index f61ec481d932a..c24d361c81744 100644 --- a/packages/react-native-renderer/src/ReactNativeGetListeners.js +++ b/packages/react-native-renderer/src/ReactNativeGetListeners.js @@ -81,7 +81,7 @@ export default function getListeners( const eventListeners = stateNode.canonical._eventListeners[registrationName]; const requestedPhaseIsCapture = phase === 'captured'; - for (let i = 0; i < eventListeners.length; i++) { + for (var i = 0; i < eventListeners.length; i++) { const listenerObj = eventListeners[i]; // Make sure phase of listener matches requested phase From e50bf6384b3a7c99233ad004c5859ad5fd6c1e2b Mon Sep 17 00:00:00 2001 From: Joshua Gross Date: Thu, 24 Feb 2022 11:50:07 -0800 Subject: [PATCH 22/27] yarn replace-fork --- packages/react-native-renderer/src/ReactNativeGetListeners.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-native-renderer/src/ReactNativeGetListeners.js b/packages/react-native-renderer/src/ReactNativeGetListeners.js index c24d361c81744..f61ec481d932a 100644 --- a/packages/react-native-renderer/src/ReactNativeGetListeners.js +++ b/packages/react-native-renderer/src/ReactNativeGetListeners.js @@ -81,7 +81,7 @@ export default function getListeners( const eventListeners = stateNode.canonical._eventListeners[registrationName]; const requestedPhaseIsCapture = phase === 'captured'; - for (var i = 0; i < eventListeners.length; i++) { + for (let i = 0; i < eventListeners.length; i++) { const listenerObj = eventListeners[i]; // Make sure phase of listener matches requested phase From f01bd0ca3521a8c3d1f7941ae34d660eb6aa81c7 Mon Sep 17 00:00:00 2001 From: Joshua Gross Date: Thu, 24 Feb 2022 11:57:35 -0800 Subject: [PATCH 23/27] turn for loop into forEach --- .../react-native-renderer/src/ReactNativeGetListeners.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/react-native-renderer/src/ReactNativeGetListeners.js b/packages/react-native-renderer/src/ReactNativeGetListeners.js index f61ec481d932a..0f1639f7354aa 100644 --- a/packages/react-native-renderer/src/ReactNativeGetListeners.js +++ b/packages/react-native-renderer/src/ReactNativeGetListeners.js @@ -81,14 +81,13 @@ export default function getListeners( const eventListeners = stateNode.canonical._eventListeners[registrationName]; const requestedPhaseIsCapture = phase === 'captured'; - for (let i = 0; i < eventListeners.length; i++) { - const listenerObj = eventListeners[i]; + eventListeners.forEach((listenerObj) => { // Make sure phase of listener matches requested phase const isCaptureEvent = listenerObj.options.capture != null && listenerObj.options.capture; if (isCaptureEvent !== requestedPhaseIsCapture) { - continue; + return; } // Only call once? @@ -120,7 +119,7 @@ export default function getListeners( } else { listeners.push(listenerObj.listener); } - } + }); } return listeners; From d1c084361f75c5da0804ed5821cd1fc8bdcc1f06 Mon Sep 17 00:00:00 2001 From: Joshua Gross Date: Thu, 24 Feb 2022 12:05:34 -0800 Subject: [PATCH 24/27] yarn prettier-all --- packages/react-native-renderer/src/ReactNativeGetListeners.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-native-renderer/src/ReactNativeGetListeners.js b/packages/react-native-renderer/src/ReactNativeGetListeners.js index 0f1639f7354aa..2fbfab9429d49 100644 --- a/packages/react-native-renderer/src/ReactNativeGetListeners.js +++ b/packages/react-native-renderer/src/ReactNativeGetListeners.js @@ -82,7 +82,7 @@ export default function getListeners( stateNode.canonical._eventListeners[registrationName]; const requestedPhaseIsCapture = phase === 'captured'; - eventListeners.forEach((listenerObj) => { + eventListeners.forEach(listenerObj => { // Make sure phase of listener matches requested phase const isCaptureEvent = listenerObj.options.capture != null && listenerObj.options.capture; From 050211d82f1146cd81b05cc293311b50710b3de2 Mon Sep 17 00:00:00 2001 From: Joshua Gross Date: Thu, 24 Feb 2022 12:40:33 -0800 Subject: [PATCH 25/27] attempt to provide a working CustomEvent jest mock so that tests can run --- .../src/ReactNativeBridgeEventPlugin.js | 1 + .../Libraries/ReactPrivate/CustomEvent.js | 14 ++++++++++++++ 2 files changed, 15 insertions(+) diff --git a/packages/react-native-renderer/src/ReactNativeBridgeEventPlugin.js b/packages/react-native-renderer/src/ReactNativeBridgeEventPlugin.js index 8f0b3af83549f..1424643c94b10 100644 --- a/packages/react-native-renderer/src/ReactNativeBridgeEventPlugin.js +++ b/packages/react-native-renderer/src/ReactNativeBridgeEventPlugin.js @@ -199,6 +199,7 @@ const ReactNativeBridgeEventPlugin = { const bubbleDispatchConfig = customBubblingEventTypes[topLevelType]; const directDispatchConfig = customDirectEventTypes[topLevelType]; let customEventConfig = null; + console.log('what is CustomEvent?', CustomEvent); if (nativeEvent instanceof CustomEvent) { // $FlowFixMe if (topLevelType.indexOf('on') !== 0) { diff --git a/packages/react-native-renderer/src/__mocks__/react-native/Libraries/ReactPrivate/CustomEvent.js b/packages/react-native-renderer/src/__mocks__/react-native/Libraries/ReactPrivate/CustomEvent.js index e69de29bb2d1d..0b0b79c5ddddf 100644 --- a/packages/react-native-renderer/src/__mocks__/react-native/Libraries/ReactPrivate/CustomEvent.js +++ b/packages/react-native-renderer/src/__mocks__/react-native/Libraries/ReactPrivate/CustomEvent.js @@ -0,0 +1,14 @@ +/** + * 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. + */ + +'use strict'; + +// See the react-native repository for a full implementation. +// This is just a stub, currently to pass `instanceof` checks. +const CustomEvent = jest.fn(); + +module.exports = {default: CustomEvent}; From b37c1af434dcc2ba143065f1f16ce627d495ff68 Mon Sep 17 00:00:00 2001 From: Joshua Gross Date: Thu, 24 Feb 2022 12:45:35 -0800 Subject: [PATCH 26/27] remove console.log --- .../react-native-renderer/src/ReactNativeBridgeEventPlugin.js | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/react-native-renderer/src/ReactNativeBridgeEventPlugin.js b/packages/react-native-renderer/src/ReactNativeBridgeEventPlugin.js index 1424643c94b10..8f0b3af83549f 100644 --- a/packages/react-native-renderer/src/ReactNativeBridgeEventPlugin.js +++ b/packages/react-native-renderer/src/ReactNativeBridgeEventPlugin.js @@ -199,7 +199,6 @@ const ReactNativeBridgeEventPlugin = { const bubbleDispatchConfig = customBubblingEventTypes[topLevelType]; const directDispatchConfig = customDirectEventTypes[topLevelType]; let customEventConfig = null; - console.log('what is CustomEvent?', CustomEvent); if (nativeEvent instanceof CustomEvent) { // $FlowFixMe if (topLevelType.indexOf('on') !== 0) { From a994a3a376a79de45d05a15a42c9f2fc88cafff1 Mon Sep 17 00:00:00 2001 From: Joshua Gross Date: Thu, 24 Feb 2022 17:00:21 -0800 Subject: [PATCH 27/27] fix capture phase registration --- .../src/ReactNativeBridgeEventPlugin.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/react-native-renderer/src/ReactNativeBridgeEventPlugin.js b/packages/react-native-renderer/src/ReactNativeBridgeEventPlugin.js index 8f0b3af83549f..7f7ee3b508013 100644 --- a/packages/react-native-renderer/src/ReactNativeBridgeEventPlugin.js +++ b/packages/react-native-renderer/src/ReactNativeBridgeEventPlugin.js @@ -213,8 +213,10 @@ const ReactNativeBridgeEventPlugin = { bubbles: nativeEvent.bubbles, phasedRegistrationNames: { bubbled: topLevelType, - // $FlowFixMe - captured: topLevelType + 'Capture', + // Unlike with the props-based handlers, capture events are registered + // to the HostComponent event emitter with the same name but a flag indicating + // that the handler is for the capture phase. + captured: topLevelType, }, }; }