-
Notifications
You must be signed in to change notification settings - Fork 27
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
Parameterized Event Handler Invocation #231
Comments
sorry, didnt see this till now! please tag me directly in issues so they show up in my sidebar. github's ui for notifications is shit. i'll try to take a look at this this weekend and let's see what makes sense to do. you're right that the synthetic event system used for parameterized handlers in domvm is incomplete. i dont think i ever had plans to add the necessary complexity to mimic the real thing. (from memory, without looking back at the code yet..it's been years): parameterized handlers are bound using single, capturing-phase listeners at the document level, so they will always fire before any handlers bound directly via if you need to have complex handling of the same events at different parts of the tree with cancellation, etc, i think the simple answer is to just use directly-bound events instead of using parameterized/capturing/simulated ones.
i think if we can get away with some relatively simple changes that are both more logical and no less performant, then i'll be happy to review a PR. there are some event tests [2] and adding some extra test cases that are resolved would be helpful. [1] https://jsfiddle.net/8f9x6qu0/ |
I can see how you might have reasoned that being a capture-phase listener, they should fire in capture-phase order, and I am arguing that this is a mistake. Container-then-contained order is rarely desirable and in 25 years of writing GUIs and GUI toolkits, I've never actually needed to execute handlers in that order. Indeed, I believe JS is the first GUI system that I've encountered that even has the option, and there I would consider capture-phase to be an IE implementation bug (I think that's where it first arose). As it stands now parameterized handlers will always fire either before or after all onxxx handlers, you couldn't do otherwise. But the handlers themselves should fire in the same order that normal onxxx handlers would have, contained before container. After thinking about it, currently one clearly can't reliably mix normal handlers and parameterized handlers in the same GUI. There is, therefore, a case to be made that DOMVM should treat all onxxx handlers as parameterized even if there is a slight performance cost to doing so; it's probably balanced out be avoiding many extra lambdas at lower levels. Once this fire-order is fixed, I certainly plan to always use Further, it seems to me that they should use a View listener not a Document listener. When it comes to mixing components from different sources, especially from non-DOMVM components, I would need a nested component's handlers to be invoked before any of its container's handlers. Say I use a pure-JS calendar component, it's crucial that all of it's key handlers get first priority and keys only bubble up to my DOMVM handlers if they were not handled by the calendar. Conversely, say my DOMVM component is used by a React program; then I need be sure that DOMVM's handlers have all been invoked before any of the React handlers (and had the opportunity to Otherwise one simply can't intermingle DOMVM with any other system, not even pure-JS components (because event handlers will be invoked in illogical, and ultimately conflicting, orders). Finally, View listeners then must be bubble-phase listeners, otherwise each view is also out of order with respect to nested views (see my argument below that they should be bubble-phase listeners). While technically a breaking change, synthetic event handlers are arguably completely broken as it sits, and these changes merely fix them. I've stewed on this for a couple of weeks now and I find myself forced to conclude that since there are times I need arguments bound to event handlers, and because we need interop with at least pure JS and possibly with some existing React code, the only alternative would be to fork DOMVM since I can't think of any way to fix this without involving the internals of the code.
This advantage must be preserved.
So in summation I think that event handling must:
(*) The programmer can use (**) So DOMVM components can properly interop with other systems, including independently mounted DOMVM views (for which we actually have a concrete use-case for customer supplied components). (***) I retract my comment about onevent invocations; I can see that the intent is to invoke after every (parameterized) event. But they should fired be after every onxxx event as well, further strengthening the case to remove the distinction between |
On the use of
In my experience this is rarely correct and safe to use without causing unintended interop problems. In general, an event handler can never know that a higher level handler does not even need to see an event. This kind of handling in other contexts has resulted in things like shift states being locked on because some event handler prevented a higher level handler from seeing the key-up which turned the state off. I strongly advise that returning However invocation must test whether stop propagation has been set and respect it, of course, and all event handler invocation must stop, including view and global invocation (which the above code does not do). |
I'm going to take a swing at a PR. |
i'm on a phone, so have not read everything, but want to make sure your pr isnt wasted effort. i dont think removing native handlers is a good idea. we need a way to stay close to the dom (principle of least astonishment), and making native handlers do magic behind the scenes should be avoided. i'm open to changing how parameterized handlers are delegated, and just using bubbling instead of capturing listeners. |
I had a feeling you'd say that, and I don't necessarily disagree. Which is why I subsequently annotated my main "solution" comment in that vein. So scratch (2), above, and having already scratched (4) & (5):
[1] Bubbling so parameterized handlers fire event after normal handlers, so embedding non-DOMVM components in DOMVM is possible (else DOMVM handlers preempt the components handlers). |
(I deleted my comment about the odd construct, because it was a result of distribution generation handling of the feature flags, not something in the source code. Sorry.) |
Digging into what's needed to have a per-View event handler registry, instead of a per-Document one... It seems to me that it might be a net gain for all nodes to have an immediate reference to their nearest VM (thus obviating the need for getVm() to walk the hierarchy). Obviously this can't be (Similarly, if the decision is made to use the mounted VM for event listeners, then it would be |
Not able to make progress on a PR. Where/when/how are the build.js and tests run? |
not sure. it potentially means more complex inter-node bookeeping to do between redraws. let's try without it first and profile.
to build you need to install node: https://nodejs.org/en/ then in the repo dir open a terminal / console and...
you can open https://github.com/domvm/domvm/blob/master/test/index.html in a browser to run all the tests. |
Node added a package-lock.json file to the repo, after following the directions above. What do you want me to do with that? Also, node reports some package vulnerabilities after auditing. Do you want me to also update the build packages to resolve these? |
plz leave as-is. i'll do the tooling updates afterwards. |
@leeoniya : The master branch has a number of tests failing for streams, before making any changes, and present for 3.4.12. How do you want me to proceed? (My changes pass all tests, but I have yet to verify that they actually change event handlers as desired.) |
if any unrelated tests fail prior to your changes, ignore them for now. verify that your changes do what you want and still pass the existing event handler tests, unless those need to be changed in some logical manner. open a PR when everything you need works as you expect and you've added extra tests to show what failed before but works after your changes. |
@leeoniya : Pull request completed. Updated fiddle showing the invocation order and interaction with native listeners: https://jsfiddle.net/LawrenceDol/usqrgf84/ BTW, I was not clear on whether the listeners will now need to be removed (I confess I almost never use add/remove listener). It seems to me that the remove of any listeners would have to be done when the element is discarded -- and the onxxx flags could be used to remove any that had been added. |
My new test fiddle unearths another fault. With nested views, the outer view's Furthermore, the |
I really have to question whether any of this complication is necessary. If a parameterized handler simply binds a listener on the actual element then the entire complexity just disappears, with
|
being auto-delegated is a key aspect of these, so it obviates 10k bindings. if you're okay with direct bindings then just:
|
OK, I'll retract that comment, although the ergonomics are much worse. I think all other considerations stand. |
agreed. TBH, browsers have changed a lot since domvm was written. perhaps the overhead of a reasonable amount of bindings (let's say 2k) is not that great, and we can remove the delegation. can you bench the difference in memory and JS cost between the two variants and maybe there's a path forward here. one concern is still being able to trigger global and vm-level onevent functions. i'm not sure how this would be done without also creating 2k wrappers around the supplied handlers. i think we'd need to keep a global |
here are timings for 10 runs of the raw dom code, one set of runs with <!doctype html>
<html>
<head>
<title>Test</title>
</head>
<body>
<script>
function onclick() {}
console.time("time");
for (let i = 0; i < 2000; i++) {
let div = document.createElement("div");
div.textContent = i;
div.addEventListener("click", onclick);
document.body.appendChild(div);
}
console.timeEnd("time");
</script>
</body>
</html> without listeners (same as single delegated): with 2k listeners: interestingly, the memory metrics look identical. if you take into account both Scripting and System timing differences, it's about 2x as expensive to have directly-bound listeners. upping this to 10k elements, the results are the same (about 2x), so this looks like the lower bound of the additional cost. also, creating 10k wrappers does not seem to have much impact: <!doctype html>
<html>
<head>
<title>Test</title>
</head>
<body>
<script>
function wrapCick(fn) {
return () => {
console.log('!');
fn();
};
}
function onclick() {}
console.time("time");
for (let i = 0; i < 10000; i++) {
let div = document.createElement("div");
div.textContent = i;
div.addEventListener("click", wrapCick(onclick));
document.body.appendChild(div);
}
console.timeEnd("time");
</script>
</body>
</html> |
There's no need for wrappers, per se. Each listener directly invokes All handlers are so invoked, so there should be little to no overhead above pure native handlers. Indeed, I would simply set the
I do not expect that binding the same function to multiple elements should have a material impact on memory use; it's just a pointer, when all is said an done, and Of course reusing a DOM element will need to wipe an PS: Without an x-axis scale, I'm not seeing a "2x more expensive"? |
there is no x axis. look at the timings in the summary. if we can keep the overhead to within what's above, i'm okay with removing the delegation/simulation. go ahead and implement the non-delegated strategy for parameterized listeners in another PR and i'll take a look 👍 |
PS, you dont need to tag me in every comment. just the first issue or PR comment. |
I'm done with work for the day, so I'll work on the pull request now. Do you have a preference for basing it on master vs. the 3.4.12 commit? |
always |
I have found
two problemsa problem with parameterized event handling, demonstrated in this fiddle.First, the global and vmonevent
handlers are invoked multiple times, once for each parameterized handler found.Second, parameterized event handlers are invoked in top-down order, from the outermost handler to the innermost, which is the opposite of normal
onxxx
event handlers.Also, I also note that the vm and global handlers are invoked regardless of whether an element-specific handler has returned false -- this may or may not be desirable/defensible, but it absolutely should be clearly documented (it might be, already, but I don't recall seeing it).
As demonstrated,
keyDown1
is invoked beforekeyDown2
. But if thebindVm
return is changed, invocation order will be the expected 2, then 1.This is hugely significant in terms of logically cooperating handlers; it's very common to want to handle an event only if a more specific handler has not already done so (as the fiddle demonstrates). This totally blindsided me, and renders parameterized event handlers of no practical use.
If it cannot be changed for reasons of compatibility, I believe a global, and per-vm option is needed to reverse this. But I, personally, would consider this a bug.
The problem seems to be in
ancestEvDefs
, usingunshift
instead ofpush
(which is also less efficient):The text was updated successfully, but these errors were encountered: