-
Notifications
You must be signed in to change notification settings - Fork 20.6k
Event: change fixHooks, propHooks to ES5 getter methods #2860
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
Conversation
This looks really good to me!
Definitely seems like a big net plus since forcing layout is much worse!
It looks like that's the case for the native event object as well? http://jsbin.com/tiwebiguli/1/edit?html,js,console,output
What's the net benefit to keeping the setter?
I'm not bothered about the way you have it now, it would be unusual for someone to infer the event type from the presence of properties wouldn't it? At least, I'd want to see someone report a bug before we tried to address this.
That seems like another place where we could wait and add the extra complexity if it turns out there's existing code that does a lot of |
} | ||
}, | ||
// Add which for click: 1 === left; 2 === middle; 3 === right | ||
// Note: button is not normalized, so don't use 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.
Might as well remove this line since we pass button
through and it was almost intended to be for end users.
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.
I've removed this line.
You're right, it looks like native events (at least in chrome, FF) already use getters on the prototype. So this would actually make jQuery Event more like native events in that sense... |
d6d4ecd
to
03ca086
Compare
I haven't had a chance to measure it yet - but I seem to recall trying it out (>2 years go now!?) and finding that invoking the getter < ~5 times was faster then invoking
Yes it would be weird. I guess I just felt it would be a bit "cleaner" if it only had the properties that it should, but I'm not sure if that is worth any extra code. The only other thing I can think of is how currently in this PR the event Sort of related... the |
}, | ||
|
||
set: function( value ) { | ||
Object.defineProperty( this, name, { |
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.
Rather than defining a setter on property access which can be a slow operation, would it be better to store the final value under a separate key on this
and just return that in the getter if it's been defined?
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.
Yeah I think that would be better, or just recompute it every time which will normally just be a lookup on originalEvent
...
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.
Agreed. Setting will be pretty rare anyway and recomputing isn't expensive, especially compared to forcing layout.
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.
Setting may not be as rare as you think, especially in unit tests
elem.trigger(jQuery.Event('click', { which: 1});
We would still need the setter, but both the setter and getter can just refer to the same key for storing the final value
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.
There are only a few situations I can think of where this is going to be in a hot path, like mousemove or pointermove events. Low-frequency events aren't going to suffer in any of these approaches I'd think. @jbedard do you have some benchmarks we could look at, even something rough? Perhaps @topaxi has something he used on #2384.
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.
I've removed the this[ name ] = value
in the getter, but kept the setter to allow things like unit tests to override the getter. This should probably still be benchmarked. I don't have any benchmarks setup right now. If anyone else wants to give it a shot then feel free! Otherwise I'll try to find some time at some point...
This definitely is redundant now. Since we're putting this in 3.0 and it's semver-major, we could just remove the per-event-type hooks and have Migrate fill this part by moving them to |
Sounds like a good idea to me. |
03ca086
to
4067fea
Compare
I've dropped support for |
prop = copy[ i ]; | ||
event[ prop ] = originalEvent[ prop ]; | ||
} | ||
|
||
// Support: Safari 6-8+ | ||
// Target should not be a text node (#504, #13143) | ||
if ( event.target.nodeType === 3 ) { |
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.
It's not directly related to this change but I feel like this Safari fix could be moved to the this.target = ...
line in jQuery.Event
. That would make jQuery.event.fix
just coordinate the prop
and fixHooks
and let jQuery.Event
do the work of constructing a standard-looking event object...
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.
It would be the caller's error for them to pass something other than an element to jQuery.Event
though, and they control if that happens. For this case we're working around a browser-generated event so although we can blame Safari we'll never get them to commit to fixing it. 😸
This looks good to me from a code/design standpoint. I'd really love to see some sort of benchmark before landing it, even if it's rather ad-hoc. @jbedard any ideas? |
Here's a super quick example comparing 2.2 vs this branch (might have to modify the <html>
<head>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.2.2/jquery.js"></script>
<script>
window.jQuery22 = $.noConflict(true);
</script>
<script src="../dist/jquery.js"></script>
<script>
window.jQuery30 = $.noConflict(true);
</script>
<style>
section {
display: inline-block;
width: 160px;
}
section div {
height: 150px;
width: 150px;
border: 1px solid black;
text-align: center;
display: block;
overflow: hidden;
}
.over {
background-color: red;
}
</style>
</head>
<body>
</body>
<script>
var PROPS = ("altKey bubbles cancelable ctrlKey detail eventPhase " +
"metaKey shiftKey view which char charCode key keyCode " +
"button buttons clientX clientY offsetX offsetY pageX pageY screenX screenY toElement").split(" ");
function setup($, name) {
var d = $("<section>").append( $("<h1>").text(name) ).appendTo("body");
d.append(
$("<div>").text("mousemove noop").on("mousemove", function(){})
);
d.append(
$("<div>").text("enterleave-toggle").on("mouseenter mouseleave", function(e) {
$(this).toggleClass("over", e.type === "mouseenter");
})
);
d.append(
$("<div>").text("props access").on("mousemove", function(e) {
$(this).text(
PROPS.map(function(p) { return p + ": " + e[p]; }).join(", ")
);
})
);
function addMousemoveListenerTest(n) {
d.append(
$("<div>").text("repeat page/offsetX access (" + n + " times)").on("mousemove", function(e) {
for (var i=0; i<n; i++) {
if (e.pageX === -1 || e.offsetX == -1) {
return;
}
}
})
);
}
for (var n=1; n<20; n+=5) {
addMousemoveListenerTest(n);
}
}
setup(jQuery22, "2.2");
setup(jQuery30, "3.0-eventfix");
</script>
</html> |
With that snippet above... every case I've tried this PR is faster then 2.2. Even if you read all properties or read |
Is it possible to provide a real-life example, like jquery/jquery.com#88 (comment) or http://jsperf.com/old-vs-new-show-hide/3. Like i don't understand what this - for (var n=1; n<20; n+=5) {
d.append(
$("<div>").text("repeat pageX access (" + n + " times)").on("mousemove", function(e) {
for (var i=0; i<n; i++) {
if (e.pageX === -1) {
return;
}
}
})
);
} supposed to show |
That tests the old fix loop which would copy everything once (including I'll try to put a jsperf together. I guess that would test invoking |
I meant how this reflect on perf overall in real app.
Yeah, if that could be something that we could mention in the blog post when releasing new version that would be great. Although testing the full functionality with Quoting @dmethvin from some time ago -
|
I would really like to land this for 3.0 rather than waiting for a later version. I don't think it is truly a breaking change but with something that changes the approach radically there is always a chance of some issues. I'd rather see those in 3.0 than 3.1. Any objections? I think this needs a quick rebase and it's ready. |
I'd say if this is a performance improvement, then it should have performance test. @dmethvin could you do a quick profile test? I heard you are an expert :) |
@dmethvin Just an observation mainly for the future; using The only reason I didn't even suggest that earlier was because you'd have divergent code paths, and the properties not explicitly listed in this file would only work in |
@dcherman Right, the divergence in behavior would be really hard to handle. If it were just possible to do something like @markelog I'm not sure I can do better than this, it's always kind of subjective as to whether a synthetic test reflects the real world. |
It is not what you said three years ago :), okay, if you sure and want this then i don't have objections |
@dmethvin For the moment, definitely seems so. Ran a JSPerf/Benchmark.js suite locally (your favorite!) and it looks like defining the |
4067fea
to
9ad80f7
Compare
This PR does have breaking changes to the event hooks API right now. Previously there was Currently this PR removes Could we drop all this functionality but leave it in jquery-migrate? jquery-migrate could easily patch If we're making API changes should we go a step further? Are there better ways of exposing this prop/hooks API? Some random ideas... |
I've added commits showing my proposals. Each one reduces the gzip size a bit more and the last one (74068a0) brings it down below master (-7 gzip/minified). 9ad80f7 is the original commit which drops 66704d5 + c24d604 drops ed7aa50 does what I proposed above in a comment. I think this is logical and it reduces size. 74068a0 completely changes the event hooks API to WDYT? |
Thanks for the awesome work, @jbedard! 👏 |
Soooo tempted to switch up the API, but I am thinking it's another big last-minute big change and we should wait until v4 to do that in order to give fair warning. To make it happen now there would need to be more work at api.jquery.com, the upgrade guide, perhaps Migrate as well, and it would need to happen quickly. One middle ground would be to:
If most of the team wants to do all of it now I'll go for it too, but we really should get the other pieces done pretty fast. And @jbedard thanks for your work on this! |
Note that this doesn't expose all properties. There is still a list of properties supported by default and any additional properties must be added manually. The good thing is that the number of properties no longer has a negative effect on performance at runtime because properties are just getters on the prototype. One reason I don't like the How about switching to |
Here is a potential (untested) jquery-migrate addition to maintain support for var originalFix = jQuery.event.fix;
jQuery.event.props = [];
jQuery.event.fixHooks = {};
jQuery.event.fix = function(originalEvent) {
var event,
type = originalEvent.type,
fixHook = this.fixHooks[ type ];
if ( jQuery.event.props.length ) {
jQuery.each( jQuery.event.props.splice( 0 ), function( i, prop ) {
jQuery.event.addProp(prop);
} );
}
if ( fixHook && fixHook.props && fixHook.props.length ) {
jQuery.each( fixHook.props.splice( 0 ), function( i, prop ) {
jQuery.event.addProp(prop);
} );
}
event = originalFix.call(this, originalEvent);
return fixHook && fixHook.filter ? fixHook.filter( event, originalEvent ) : event
}; |
|
||
return event.pageX; | ||
}, | ||
addProp: function( name, hook ) { |
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 concern here is that we're assuming nobody wants to take an existing hook.
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.
The existing hooks only ensure which
/pageX
/pageY
properties (which this PR does automatically for all events, making reuse pointless), and aren't even defined for an event type until first dispatch: https://jsfiddle.net/1mayydrz/ . Heck, fixHooks
is only documented at https://learn.jquery.com/events/event-extensions/#jquery-event-fixhooks-object (i.e., not at api.jquery.com). I think these changes are safe for 3.0.
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.
The docs you referenced mention to take care not to clobber anything and give an example, but the proposed API doesn't provide any way to ensure that or even know if there was a previous hook. However, the two fixHooks plugins I could find quickly on GitHub (https://github.com/timmywil/jquery.event.pointertouch/blob/master/pointertouch.js and https://github.com/aarongloege/jquery.touchHooks/blob/master/jquery.touchHooks.js) just set it blindly. 😸
Maybe a force
boolean argument could specify whether to clobber an existing hook? I don't think it needs to be any more complicated than that. The scenario I could see is that someone adds a hook that we later incorporate into jQuery with additional functionality, it would work better if their hook was ignored for that case.
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.
The docs you referenced mention to take care not to clobber anything and give an example
That's true, but the proposed API no longer varies by event type. What kind of clobbering are you concerned about?
the proposed API doesn't provide any way to ensure that or even know if there was a previous hook.
Object.prototype.hasOwnProperty.call( jQuery.Event.prototype, name )
Maybe a
force
boolean argument could specify whether to clobber an existing hook? I don't think it needs to be any more complicated than that. The scenario I could see is that someone adds a hook that we later incorporate into jQuery with additional functionality, it would work better if their hook was ignored for that case.
Seems like overkill to me, but easy enough to include for I'm guessing about 12 bytes. Does this mean you're also convinced on jQuery.event.addProp
, though?
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.
What kind of clobbering are you concerned about?
Ones where we decide to add a hook for some property in later versions but there might have already been previous attempts by others to fix the same problem and they overwrite our better solution after jQuery loads.
Does this mean you're also convinced on jQuery.event.addProp, though?
I was going to say that if we're going to publicize jQuery.Event.prototype
has the hooks we might as well just tell people to modify it themselves, but since the lazy hook code is tricky that's not a good idea. So yeah, I'll go for it. At minimum we need to fill fixHooks
though via Migrate though. It's a rather obscure interface but people were using 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.
I think supporting fixHooks
and props
in migrate should be simple if we go that route (This snippet is a potential migrate example).
We could also look for alternates to addProp(name[, hook])
such as those I mentioned in a previous comment. For example jQuery.event.props[type] = true | getterMethod
would allow easier access to existing getters, but then we need to check for new props
in fix
.
That is the nice thing about addProp(name[, hook])
. It does everything in this setup method, and nothing at event/fix time. This also allows creating separate and simpler getters for the hook vs no-hook cases.
Other ideas...
- Change
hook( this.originalEvent )
tohook( this.originalEvent, wrappedHook || propGetter )
so overridden hooks have runtime access to the previous hook. This might require storing the hooks somewhere other then the closure of the getter. - Add other params to to
hook
like the propertyname
?
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 of those is mine, and I forgot it was there, just to throw that out there.
Per yesterday's meeting this is good to land, all commits right? |
Should probably squash the commits since there's some intermediate steps (the Let me know if there's anything else I can do to help. |
Yep, I squashed them but having the individual commits here is handy since it matches the discusion. After squashing I'm getting a test failure on a fixHooks test, should that test have been removed? |
That specific test should probably just be removed. Might be worth adding some |
Yes, we definitely need some tests. I'll move the propHooks test to Migrate to be sure we're shimming that correctly. |
- removes the copy loop in jQuery.event.fix - avoids accessing properties such as client/offset/page/screen X/Y which may cause style recalc or layouts
I've added 0a213d2 which reduces the size a bit down to -13 min/gz |
Fixes jquerygh-3103 Fixes jquerygh-1746 Closes jquerygh-2860 - Removes the copy loop in jQuery.event.fix - Avoids accessing properties such as client/offset/page/screen X/Y which may cause style recalc or layouts - Simplifies adding property hooks to event object (cherry-picked from e61fccb)
This is a potential solution for #1746. @dmethvin also mentioned using this for #2337. This also removes the slow (even if no layout is forced) loop from
fix
.The main problems with this approach:
fix
loop, and fixes jQuery.event.fix() may force layout #1746)putting getter properties on the prototype means(EDIT: this is actually what native events do, so this is probably a good thing)event.hasOwnProperty(eventProp)
is falsePotential changes to this approach:
jQuery.MouseEvent
andjQuery.KeyboardEvent
so properties likeshiftKey
are only on keyboard events