-
Notifications
You must be signed in to change notification settings - Fork 49.8k
[WIP] React Native: Turn HostComponent into an EventEmitter #23278
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
Changes from 2 commits
a33b351
83e9bc9
d73dd66
d35b766
29bf6a4
049a50b
88cfd64
f79c3fc
7f2a1b3
7f3a82c
5a8047d
3f38d4a
1055e56
f1746d2
df4a1fd
e5f5199
901742a
a78b098
633f9eb
7adec88
5bc7103
e50bf63
f01bd0c
d1c0843
050211d
b37c1af
a994a3a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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]?.length > 0) { | ||
JoshuaGross marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| 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); | ||
| } | ||
| }; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Even if we initialize it lazily, I'm not sure I feel great about adding a new field to every single host instance, even if it's set to null. We're trying to minimize memory usage. And this is doing that for a (relatively uncommon) feature in the tree. I wonder if there's any way we could only pay the cost for the components using it. E.g. some sort of a Map outside. I guess it would have to be a WeakMap. I think we need to get @sebmarkbage opinion on this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One extra field is probably not the biggest deal here. The big deal is that this object exists at all and that methods use virtual dispatch and that all methods must always be loaded instead of lazily.
The goal was to get rid of it but if the goal is to preserve DOM-like looks, then maybe the whole object can at least be made lazy only if there are refs on it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
By "this object" do you mean the ReactFabricHostComponent object? Can you describe what the alternative would be - just using a raw JSI-bridged native object? I was not aware of those plans. Worth discussing that more for sure if we want to move further in that direction, I'd like to hear pros/cons and it'd be good to have an idea of what the cost of this class and fields are
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you mean something like - ReactFabricHostComponent is not instantiated for a given ShadowNode until/unless it is requested via the
refprop? That is a pretty interesting optimization that I would be happy to drive (in a few months - I'm going on leave soon) - I would be happy with that, hopefully we agree that that is outside of the scope of this PR?