-
-
Notifications
You must be signed in to change notification settings - Fork 1.6k
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
feat: DOM Events #10100
base: main
Are you sure you want to change the base?
feat: DOM Events #10100
Conversation
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.
This is looking great already! I've given it a read and noted a few things for consideration. I'll try to give this a more thorough look and try it as well.
Awesome job 🎉 🥳
cdabf8e
to
de169c2
Compare
be58333
to
6128cc7
Compare
6e7899b
to
f22db38
Compare
☁️ Nx Cloud ReportCI is running/has finished running commands for commit 96f841d. As they complete they will appear below. Click to see the status, the terminal output, and the build insights. 📂 See all runs for this branch ✅ Successfully ran 2 targetsSent with 💌 from NxCloud. |
92aa472
to
fecc4de
Compare
7fae871
to
e2c3476
Compare
PerformanceIt's as optimised as I can make itI've profiled the DOMEvent class as thoroughly as I can, and I can find no wasted logic at this point. You can see comments on just about every line showing the approaches I've compared (I can either remove these later or keep them in place to protect against the code being blindly refactored in future). Yes, it's slower than beforeTo be clear, the DOM Events implementation is slower than the original single-target notification system. That's to be expected when we have multiple targets rather than just one. How much slower?If you call
So even if some flavours of NativeScript (e.g. Core) wanted to forever opt out of bubbling/capturing, it would still take 3x the time of the original. But, in absolute terms, the fully-featured version still only takes 3.750 microseconds to dispatch an event. For some perspective, this is equivalent to running 38 functions in sequence that simply add one number to another. For another comparison, in the grand scheme of human reaction speed (measured in milliseconds) - and this may be flawed reasoning - you'd have to be firing thousands of events in one go to block the main thread perceptibly. Given that it easily brings in 10x the functionality, I think a 6x slowdown factor is defendable, and even more so as it paves the way for DOM conformance and JS runtime alignment. But maybe the reviewers have some ideas for making it faster! How to profile for yourselfI've included two apps in the PR (which, again can be removed or refactored) to regression-test performance - they're called
Just click the "Automated profiling" button and read the console to run the profiling. |
86dc315
to
90457c2
Compare
53f4f73
to
fbdf0f5
Compare
0395d7c
to
ffd9f4c
Compare
98ae099
to
96f841d
Compare
PR Checklist
What is the current behavior?
The current Events system notifies only the direct target of the event (and any global event listeners). Gestures work a little bit differently to other events, as they are optimised for using without redundantly creating extra gesture listeners when existing ones could be reused.
What is the new behavior?
In summary, Events (including gestures) are now based on DOM EventTarget, and fully support capturing, bubbling, and cancelling.
I'll go into detail below.
Event propagation
Below I'll compare the behaviour before (as a refresher) and after this PR.
Button
;myButton
;Button
.Button
;[rootView, myStack]
;myButton
;[myStack, rootView]
;Button
.Performance impact
Not yet measured, but hopefully we can put something together after cutting an early-access release.
Speed
The new Event propagation obviously performs more work than before, but it's still pretty trivial stuff (just walking a linked list of nodes and evaluating for each one whether there are any eligible listeners for the event currently being dispatched).
Memory usage
A little more memory will be used than before, as the callbacks are now wrapped in a DOMEvent instance. I think this is a small cost.
Room for optimisation
I've written this PR first and foremost to be readable, so there is certainly scope to do a second pass on it to optimise any hot paths. I think the algorithms are sound, so any significant savings would likely have to be a culmination of small optimisations.
Events API
Changes to Observable
The following changes were made in order to support
class Observer implements EventTarget
:Observable
methods:addEventListener
:options?: AddEventListenerOptions | boolean
off
:options?: EventListenerOptions | boolean
on
:options?: AddEventListenerOptions | boolean
once
:options?: AddEventListenerOptions | boolean
removeEventListener
:options?: EventListenerOptions | boolean
prototype.addEventListener
:options?: AddEventListenerOptions | boolean
EventListenerOrEventListenerObject | ((data: EventData) => void)
prototype.dispatchEvent
:prototype.notify
:options?: CustomEventInit
prototype.off
:options?: EventListenerOptions | boolean
prototype.on
:options?: AddEventListenerOptions | boolean
prototype.once
:options?: AddEventListenerOptions | boolean
prototype.removeEventListener
:options?: EventListenerOptions | boolean
EventListenerOrEventListenerObject | ((data: EventData) => void)
The presence of
thisArg
means that these APIs don't map exactly to DOM, but perhaps due to the various optional params or non-strict mode, TypeScript is satisfied that the class implementsEventTarget
.Although the typings claim to support
EventListenerOrEventListenerObject
, for the sake of backwards compatibility, we handle the callback strictly as if it were(data: EventData) => void
- so please don't pass in(event: Event) => void
as it won't go well! My hope would be to migrate to the latter in a major version update, however.You may ask, then "how do I get hold of the DOM Event, then?" if the callback gives you an EventData. See the following section for that.
New features
EventData callbacks are now wrapped in a DOM Event. To access the DOM Event for the currently-running callback, use this API for now (in a future release, as a breaking change, my intention would be to pass the DOM Event instead of the EventData in the callback):
Propagation of DOM Events can be stopped via calling
domEvent.stopPropagation()
ordomEvent.stopImmediatePropagation()
as per the DOM Events spec.Native effects relating to the event can be cancelled by calling
domEvent.preventDefault()
as per the DOM Events spec. However, for now, all events are created as non-cancellable, as cancellability will have to be implemented on a case-by-case basis.Events can be listened for in either the bubbling or capture phases, based on the options passed into
Observable.prototype.addEventListener()
.Breaking changes
Removal/restriction of some internal APIs
The listener records in Observable, ViewBase, and GesturesObserver were exposed to external consumers - these are now made private, with certain APIs for accessing them removed. I think it is unlikely that these will have been used outside of Core, in any case.
Corrections to EventData and callback typings
In the course of this PR, I fixed many long-incorrect event listener typings and simplified the EventData typings. So all the old mentions of
object?: Partial<Observable>
are now simplyobject: Observable
. This is a good thing, but may require some adjustments for existing NativeScript projects.Deprecation notices
I plan to follow up this PR with another one that removes the static EventTarget-like methods, namely:
This is because they deviate from the EventTarget spec while adding little value. The only usage of it in Core is for Application - in a follow-up PR, Application will be refactored into an Observable to remove the need for it. If this functionality is needed for a particular class after all, consider extending that class and overriding the
notify
method to your needs.I would hope also to deprecate
thisArg
, but we will have to discuss how to go about that.