Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

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

Merged
merged 20 commits into from
Mar 15, 2019

Conversation

trueadm
Copy link
Contributor

@trueadm trueadm commented Mar 6, 2019

Summary

This PR makes a bunch of changes to the underlying implementation for how ReactDOM listens to events on the DOM. Notably, it opens up the possibility of exploring the proper handling of passive/non-passive event listeners. As to not break existing event listeners, they now fall under "legacy", where they work 100% as they do now without any changes in control flow. Also some old code has been tidied up and Flow annotations have been added where they were previously missing.

Why do we need this?

For future event API exploration we need to properly hook into controlling how event listeners are registered with respect to if they are passive or non-passive. The way ReactDOM deals with events now is to not specify if an event is passive or non-passive. By doing so, we let the browser determine the behaviour behind the scenes. Common cases like when touchmove is registered by React to the document mean browsers actually make the events passive behind the scenes. This is the ideal case, but in the future we also need ways of controlling this behaviour for future event API work.

Validate this works

This PR is a part of a larger part of work which has implementations and tests that validate that the passive/non-passive logic works. In the interests of keeping this PR slim, I've intentionally omitted that code. In further follow up PRs, I'll add that logic to validate the behaviour of this PR. I know this isn't the most ideal case, but it was the best approach I could come up with.

Polyfills

In the future, if we decide to ship an API that makes use of passive events, support for addEventListener and specifically the third argument on addEventListener where the argument is an object needs to supported. There is a polyfill we could advise people to use or we could try and check for this in DEV and warn. I'm not fully sure on the path we should take yet - maybe it's something we should consider in the future once we're ready to ship said features.

@trueadm trueadm requested a review from sophiebits March 6, 2019 16:06
Copy link
Contributor

@philipp-spiess philipp-spiess left a comment

Choose a reason for hiding this comment

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

This is great 🎉 I can't wait to see how we gonna use the new implementation.

The code path for legacy listeners looks pretty much unchanged expect that we now require WeakMap instead of using the expando.

Do we know which browsers we want to support in the new implementation? Based on that we might need a few feature tests.

packages/react-dom/src/events/EventListener.js Outdated Show resolved Hide resolved
packages/react-dom/src/events/ReactBrowserEventEmitter.js Outdated Show resolved Hide resolved
packages/react-dom/src/events/ReactBrowserEventEmitter.js Outdated Show resolved Hide resolved
packages/react-dom/src/events/ReactDOMEventListener.js Outdated Show resolved Hide resolved
Copy link
Contributor

@nhunzaker nhunzaker left a comment

Choose a reason for hiding this comment

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

Awesome to see this work happening, 👍

@trueadm
Copy link
Contributor Author

trueadm commented Mar 13, 2019

As per the request of @gaearon, I've run a bunch of performance tests on this PR vs master. I slightly optimized the approach and now I see a slight performance gain in Chrome and Safari. No noticeable differences in Edge and Firefox.

I'd really appreciate it if we could get this PR landed soon.

@nhunzaker
Copy link
Contributor

Where did you land on feature checking event listener options objects? Adding a feature detection like the MDN recommendation seems fine and with minimal impact.

Also if passive events won't be publicly exposed for a while maybe it could be kicked down the road a bit.

Does this need additional testing? I'd be happy to give it a run across browsers.

@trueadm
Copy link
Contributor Author

trueadm commented Mar 13, 2019

@nhunzaker I added that support just now. It would be awesome if you could do some manual tests for it too :)

packages/events/EventPluginHub.js Outdated Show resolved Hide resolved
packages/events/EventPluginHub.js Outdated Show resolved Hide resolved
packages/events/ResponderEventPlugin.js Outdated Show resolved Hide resolved
packages/react-dom/src/events/ReactBrowserEventEmitter.js Outdated Show resolved Hide resolved
packages/react-dom/src/events/ReactDOMEventListener.js Outdated Show resolved Hide resolved
packages/react-dom/src/events/SimpleEventPlugin.js Outdated Show resolved Hide resolved
@trueadm
Copy link
Contributor Author

trueadm commented Mar 13, 2019

I've updated the PR. I've removed the confusing null | boolean logic for passive and instead went with passing through an enum, which shows intent better. I've also addressed the PR feedback. There are no actors using the new passive/non-passive event system yet, but that will come soon with the experimental event API proposal I have brewing (which also doesn't require the event plugin system too).

Copy link
Contributor

@philipp-spiess philipp-spiess left a comment

Choose a reason for hiding this comment

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

Nice! The enum helped to clear up my last questions as well.

Copy link
Contributor

@threepointone threepointone left a comment

Choose a reason for hiding this comment

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

this lgtm. I don't have context around some of it, but the changes look fine. bugging @trueadm for a walkthrough today.

props = rawProps;
break;
case 'input':
ReactDOMInputInitWrapperState(domElement, rawProps);
props = ReactDOMInputGetHostProps(domElement, rawProps);
trapBubbledEvent(TOP_INVALID, domElement);
trapBubbledEvent(TOP_INVALID, domElement, true);
Copy link
Collaborator

@gaearon gaearon Mar 14, 2019

Choose a reason for hiding this comment

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

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

Copy link
Collaborator

@sebmarkbage sebmarkbage left a comment

Choose a reason for hiding this comment

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

I'm still reviewing the implementation details but can we wrap some of the forked branches in a featureFlag before landing? A small increase is expected but this is increasing a little more than I'd expect for a flag that's turned off. Presumably it'll keep growing as you add more requirements/features so good to make sure the boundaries are clear from the beginning.

@trueadm
Copy link
Contributor Author

trueadm commented Mar 14, 2019

@gaearon @sebmarkbage I've re-written the implementation based on feedback you both gave me. It now uses bitwise flags to express the intention of the event system. Furthermore, everything should almost now get DCE due to enableEventAPI. Thanks!

@trueadm
Copy link
Contributor Author

trueadm commented Mar 14, 2019

Nice, this actually makes ReactDOM 0.1% smaller :P

): void {
EventListenerWWW.listen(element, eventType, listener);
EventListenerWWW.listen(element, eventType, listener, passive);
Copy link
Collaborator

@sebmarkbage sebmarkbage Mar 15, 2019

Choose a reason for hiding this comment

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

This doesn't seem right. The internal EventListener module doesn't accept a 4th argument (and Event.listen accepts a forth argument which is a priority and won't work with this). What's your plan here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I’ll update the www version

Copy link
Collaborator

@sebmarkbage sebmarkbage left a comment

Choose a reason for hiding this comment

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

Seems fine to me. Just make sure it works with www before you land so we can sync.

@trueadm trueadm merged commit 371bbf3 into facebook:master Mar 15, 2019
@trueadm trueadm deleted the passive-event-listening branch March 15, 2019 09:39
// supported too. This way the responder event plugins know,
// and can provide polyfills if needed.
if (passive) {
if (passiveBrowserEventsSupported) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

This seems like the wrong check to me. Shouldn't this case be inverted? This will mark it as active and PASSIVE_NOT_SUPPORTED only if passive events are supported.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Well spotted!

export const RESPONDER_EVENT_SYSTEM = 1 << 1;
export const IS_PASSIVE = 1 << 2;
export const IS_ACTIVE = 1 << 3;
export const PASSIVE_NOT_SUPPORTED = 1 << 4;
Copy link
Contributor

@NE-SmallTown NE-SmallTown Apr 2, 2019

Choose a reason for hiding this comment

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

why we don't use literal number(and add comment) here just like maxSigned31BitInt?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

These are used to do bitwise operations on, as there can be a combination of the above being used.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I think the question was why 1 << 4 is used over 16. 1 << 4 makes the intent clearer. maxSigned31BitInt "solves" this by adding a comment. I don't think it matters what pattern is used.

Copy link
Contributor

Choose a reason for hiding this comment

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

yea, just like @eps1lon said

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This pattern is clear, as we may add more in the future, which is simply 1 << 5, 1 << 6 etc. We use this approach throughout the React codebase and we've verified that Google Closure Compiler simplifies the constant value.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

9 participants