Skip to content

Effects: use requestAnimationFrame timestamp if available #3151

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

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Gruntfile.js
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ module.exports = function( grunt ) {
"deprecated",
"dimensions",
"effects",
"effects-nostubs",
"event",
"manipulation",
"offset",
Expand Down
19 changes: 13 additions & 6 deletions src/effects.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,24 +27,31 @@ var
rfxtypes = /^(?:toggle|show|hide)$/,
rrun = /queueHooks$/;

function schedule() {
function schedule( timestamp ) {
if ( inProgress ) {
if ( document.hidden === false && window.requestAnimationFrame ) {
if ( document.hidden === false ) {
window.requestAnimationFrame( schedule );
} else {
window.setTimeout( schedule, jQuery.fx.interval );
}

jQuery.fx.tick();
jQuery.fx.tick( timestamp );
}
}

// We need to be using Date.now() or performance.now() consistently as they return different
// values: performance.now() counter starts on page load.
// Support: IE <10, Safari <8.0, iOS <9, Android <4.4, Node with jsdom 9.4
function getTimestamp() {
Copy link
Member

Choose a reason for hiding this comment

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

Can we just use jQuery.now() for all the browsers that don't have the rAF timestamp? Which browsers are affected?

Copy link
Member Author

Choose a reason for hiding this comment

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

This function is also used in createFxNow() which, I think, is not limited to old browsers?

Copy link
Member

Choose a reason for hiding this comment

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

Given the comments, shouldn't we have something more like this?

getTimestamp = window.performance && typeof window.performance.now === "function" ?

    // or window.performance.now.bind( window.performance )
    jQuery.proxy( window.performance, "now" ) :

    jQuery.now;

If the complaint is about testing, then we just need an iframe test in which the sandbox is mocked before loading jQuery.

Copy link
Member Author

Choose a reason for hiding this comment

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

I guess it should work, I'll try it.

Copy link
Member Author

Choose a reason for hiding this comment

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

Unfortunately, test setup is run after jQuery is sourced so this would freeze getTimestamp to use the initial, non-mocked window.performance.

Copy link
Member Author

Choose a reason for hiding this comment

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

I could put the if outside of the function definition but that only adds 3 bytes and I suppose such a single if will not cause any grief for any relatively modern browser.

The fact that we can't reliably mock stuff before jQuery is loaded is obviously a problem but this is an issue bigger than this PR.

return window.performance.now();
}

// Animations created synchronously will run synchronously
function createFxNow() {
window.setTimeout( function() {
fxNow = undefined;
} );
return ( fxNow = Date.now() );
return ( fxNow = getTimestamp() );
}

// Generate parameters to create a standard animation
Expand Down Expand Up @@ -644,12 +651,12 @@ jQuery.each( {
} );

jQuery.timers = [];
jQuery.fx.tick = function() {
jQuery.fx.tick = function( timestamp ) {
var timer,
i = 0,
timers = jQuery.timers;

fxNow = Date.now();
fxNow = timestamp || getTimestamp();
Copy link
Member

Choose a reason for hiding this comment

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

I'm wondering if still needs this, since this change was added for the old ipad zooming thing - https://bugs.jquery.com/ticket/12837, maybe it's not required anymore. Or should we add Support comment?

Copy link
Member Author

Choose a reason for hiding this comment

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

Just removing this line (& the one setting fxNow to undefined at the end) makes Chrome fail most of effects tests.

Copy link
Member Author

Choose a reason for hiding this comment

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

I could optionally change getTimestamp() here to jQuery.now() as timestamp will always be defined if tick is called by jQuery & rAF & perfomance.now() are supported. But this seems safer, esp. that tick is exposed.


for ( ; i < timers.length; i++ ) {
timer = timers[ i ];
Expand Down
38 changes: 38 additions & 0 deletions test/data/effects/matchingTimestamps.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en" dir="ltr" id="html">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>Effects: matching timestamps</title>
</head>
<body>
<script src="../../jquery.js"></script>
<script src="../iframeTest.js"></script>
<div id="test-div" style="height: 1000px;"></div>
<script>
//<![CDATA[
setTimeout( function () {

// Handle a timeout.
startIframeTest( false, "The test timed out" );
}, 5000 );

var maxNow = 0;

jQuery( "#test-div" ).animate( {
height: '2000px',
}, {
duration: 300,
step: function( now ) {
if ( maxNow - now > 100 ) {
startIframeTest( false, "Animation is stepping back from " + maxNow + " to " + now );
}
maxNow = Math.max( now, maxNow );
},
complete: function() {
startIframeTest( true );
}
} );
//]]>
</script>
</body>
</html>
8 changes: 3 additions & 5 deletions test/unit/animation.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,9 @@ if ( !jQuery.fx ) {
return;
}

var oldRaf = window.requestAnimationFrame,
defaultPrefilter = jQuery.Animation.prefilters[ 0 ],
defaultTweener = jQuery.Animation.tweeners[ "*" ][ 0 ],
startTime = 505877050;
var defaultPrefilter = jQuery.Animation.prefilters[ 0 ];
var defaultTweener = jQuery.Animation.tweeners[ "*" ][ 0 ];
var startTime = 505877050;

// This module tests jQuery.Animation and the corresponding 1.8+ effects APIs
QUnit.module( "animation", {
Expand All @@ -26,7 +25,6 @@ QUnit.module( "animation", {
this.sandbox.restore();
jQuery.fx.stop();
jQuery.fx.interval = this._oldInterval;
window.requestAnimationFrame = oldRaf;
return moduleTeardown.apply( this, arguments );
}
} );
Expand Down
46 changes: 38 additions & 8 deletions test/unit/effects.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,29 +5,42 @@ if ( !jQuery.fx ) {
return;
}

var oldRaf = window.requestAnimationFrame,
hideOptions = {
inline: function() { jQuery.style( this, "display", "none" ); },
cascade: function() { this.className = "hidden"; }
};
var hideOptions = {
inline: function() { jQuery.style( this, "display", "none" ); },
cascade: function() { this.className = "hidden"; }
};

QUnit.module( "effects", {
beforeEach: function() {
this.sandbox = sinon.createSandbox();
this.clock = this.sandbox.useFakeTimers( 505877050 );

this._oldInterval = jQuery.fx.interval;
window.requestAnimationFrame = null;
jQuery.fx.step = {};
jQuery.fx.interval = 10;

// Simulate rAF using the mocked setTimeout as otherwise we couldn't test
// the rAF + performance.now code path.
this.sandbox
.stub( window, "requestAnimationFrame" )
.callsFake( function( callback ) {
return window.setTimeout( callback, jQuery.fx.interval,
Date.now() - 99989.6394 );
} );

// Fake performance.now() returning lower values than Date.now()
// and that its values are fractional.
this.sandbox.stub( window.performance, "now" ).callsFake( function() {
return Date.now() - 99999.6394;
} );
},
afterEach: function() {
this.sandbox.restore();
jQuery.fx.stop();
jQuery.fx.interval = this._oldInterval;
window.requestAnimationFrame = oldRaf;
return moduleTeardown.apply( this, arguments );
}
} );
}, function() {

QUnit[ QUnit.jQuerySelectors ? "test" : "skip" ]( "sanity check", function( assert ) {
assert.expect( 1 );
Expand Down Expand Up @@ -2623,4 +2636,21 @@ QUnit.test( "jQuery.speed() - durations", function( assert ) {
jQuery.fx.off = false;
} );

} );

QUnit.module( "effects-nostubs", function() {

testIframe(
"matching timestamp",
"effects/matchingTimestamps.html",
function( assert, framejQuery, frameWin, frameDoc, success, failureReason ) {
assert.expect( 1 );

assert.ok( success, failureReason );
}
);

} );

} )();

4 changes: 0 additions & 4 deletions test/unit/tween.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,18 @@ if ( !jQuery.fx ) {
return;
}

var oldRaf = window.requestAnimationFrame;

QUnit.module( "tween", {
beforeEach: function() {
this.sandbox = sinon.createSandbox();
this.clock = this.sandbox.useFakeTimers( 505877050 );
this._oldInterval = jQuery.fx.interval;
window.requestAnimationFrame = null;
jQuery.fx.step = {};
jQuery.fx.interval = 10;
},
afterEach: function() {
this.sandbox.restore();
jQuery.fx.stop();
jQuery.fx.interval = this._oldInterval;
window.requestAnimationFrame = oldRaf;
return moduleTeardown.apply( this, arguments );
}
} );
Expand Down