Skip to content

Commit eca2f57

Browse files
committed
Effects: Use requestAnimationFrame timestamp if available
In some environments that support the requestAnimationFrame timestamp callback parameter using it results in smoother animations. Note: the rAF timestamp is using the same API as performance.now() under the hood so they're compatible with each other. However, some browsers support rAF (with a timestamp parameter) but not performance.now() so using them both would introduce an error. This commit stops using rAF in browsers that don't support performance.now(). From all the browsers jQuery supports this only affects iOS <9 (currently less than 5% of all iOS users) which will now not use rAF. Fixes gh-3143 Closes gh-3151
1 parent 543d3d2 commit eca2f57

File tree

8 files changed

+153
-39
lines changed

8 files changed

+153
-39
lines changed

Gruntfile.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,7 @@ module.exports = function( grunt ) {
137137
"deprecated",
138138
"dimensions",
139139
"effects",
140+
"effects-nostubs",
140141
"event",
141142
"manipulation",
142143
"offset",

src/effects.js

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ define( [
1111
"./css/adjustCSS",
1212
"./data/var/dataPriv",
1313
"./css/showHide",
14+
"./effects/support",
1415

1516
"./core/init",
1617
"./queue",
@@ -20,7 +21,7 @@ define( [
2021
"./css",
2122
"./effects/Tween"
2223
], function( jQuery, camelCase, document, isFunction, rcssNum, rnothtmlwhite, cssExpand,
23-
isHiddenWithinTree, swap, adjustCSS, dataPriv, showHide ) {
24+
isHiddenWithinTree, swap, adjustCSS, dataPriv, showHide, support ) {
2425

2526
"use strict";
2627

@@ -29,24 +30,34 @@ var
2930
rfxtypes = /^(?:toggle|show|hide)$/,
3031
rrun = /queueHooks$/;
3132

32-
function schedule() {
33+
function schedule( timestamp ) {
3334
if ( inProgress ) {
34-
if ( document.hidden === false && window.requestAnimationFrame ) {
35+
if ( support.smoothAnim && document.hidden === false ) {
3536
window.requestAnimationFrame( schedule );
3637
} else {
3738
window.setTimeout( schedule, jQuery.fx.interval );
3839
}
3940

40-
jQuery.fx.tick();
41+
jQuery.fx.tick( timestamp );
4142
}
4243
}
4344

45+
// We need to be using jQuery.now() or performance.now() consistently as they return different
46+
// values: performance.now() counter starts on page load.
47+
// Support: IE <10, Safari <8.0, iOS <9, Android <4.4, Node with jsdom 9.4
48+
function getTimestamp() {
49+
if ( support.smoothAnim ) {
50+
return window.performance.now();
51+
}
52+
return Date.now();
53+
}
54+
4455
// Animations created synchronously will run synchronously
4556
function createFxNow() {
4657
window.setTimeout( function() {
4758
fxNow = undefined;
4859
} );
49-
return ( fxNow = Date.now() );
60+
return ( fxNow = getTimestamp() );
5061
}
5162

5263
// Generate parameters to create a standard animation
@@ -649,12 +660,12 @@ jQuery.each( {
649660
} );
650661

651662
jQuery.timers = [];
652-
jQuery.fx.tick = function() {
663+
jQuery.fx.tick = function( timestamp ) {
653664
var timer,
654665
i = 0,
655666
timers = jQuery.timers;
656667

657-
fxNow = Date.now();
668+
fxNow = timestamp || getTimestamp();
658669

659670
for ( ; i < timers.length; i++ ) {
660671
timer = timers[ i ];

src/effects/support.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
define( [
2+
"../var/support"
3+
], function( support ) {
4+
5+
"use strict";
6+
7+
// Support: IE <10, Safari <8.0, iOS <9, Android <4.4, Node with jsdom 9.4
8+
// The rAF timestamp is using the same API as performance.now() under the
9+
// hood so they're compatible with each other. However, some browsers support rAF
10+
// (with a timestamp parameter) but not performance.now() so using them both
11+
// would introduce an error. That's why we use a single support test for both
12+
// and don't use rAF if performance.now() is not supported.
13+
support.smoothAnim = !!( window.requestAnimationFrame && window.performance );
14+
15+
return support;
16+
} );
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
2+
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en" dir="ltr" id="html">
3+
<head>
4+
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
5+
<title>Effects: matching timestamps</title>
6+
</head>
7+
<body>
8+
<script src="../../jquery.js"></script>
9+
<script src="../iframeTest.js"></script>
10+
<div id="test-div" style="height: 1000px;"></div>
11+
<script>
12+
//<![CDATA[
13+
setTimeout( function () {
14+
15+
// Handle a timeout.
16+
startIframeTest( false, "The test timed out" );
17+
}, 5000 );
18+
19+
var maxNow = 0;
20+
21+
jQuery( "#test-div" ).animate( {
22+
height: '2000px',
23+
}, {
24+
duration: 300,
25+
step: function( now ) {
26+
if ( maxNow - now > 100 ) {
27+
startIframeTest( false, "Animation is stepping back from " + maxNow + " to " + now );
28+
}
29+
maxNow = Math.max( now, maxNow );
30+
},
31+
complete: function() {
32+
startIframeTest( true );
33+
}
34+
} );
35+
//]]>
36+
</script>
37+
</body>
38+
</html>

test/unit/animation.js

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,15 @@ if ( !jQuery.fx ) {
55
return;
66
}
77

8-
var oldRaf = window.requestAnimationFrame,
9-
defaultPrefilter = jQuery.Animation.prefilters[ 0 ],
10-
defaultTweener = jQuery.Animation.tweeners[ "*" ][ 0 ],
11-
startTime = 505877050;
8+
var oldSmoothAnim = jQuery.support.smoothAnim;
9+
var defaultPrefilter = jQuery.Animation.prefilters[ 0 ];
10+
var defaultTweener = jQuery.Animation.tweeners[ "*" ][ 0 ];
11+
var startTime = 505877050;
1212

1313
// This module tests jQuery.Animation and the corresponding 1.8+ effects APIs
1414
QUnit.module( "animation", {
1515
setup: function() {
16-
window.requestAnimationFrame = null;
16+
jQuery.support.smoothAnim = false;
1717
this.sandbox = sinon.sandbox.create();
1818
this.clock = this.sandbox.useFakeTimers( startTime );
1919
this._oldInterval = jQuery.fx.interval;
@@ -26,7 +26,7 @@ QUnit.module( "animation", {
2626
this.sandbox.restore();
2727
jQuery.fx.stop();
2828
jQuery.fx.interval = this._oldInterval;
29-
window.requestAnimationFrame = oldRaf;
29+
jQuery.support.smoothAnim = oldSmoothAnim;
3030
return moduleTeardown.apply( this, arguments );
3131
}
3232
} );

test/unit/effects.js

Lines changed: 45 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,29 +5,47 @@ if ( !jQuery.fx ) {
55
return;
66
}
77

8-
var oldRaf = window.requestAnimationFrame,
9-
hideOptions = {
10-
inline: function() { jQuery.style( this, "display", "none" ); },
11-
cascade: function() { this.className = "hidden"; }
12-
};
8+
var hideOptions = {
9+
inline: function() { jQuery.style( this, "display", "none" ); },
10+
cascade: function() { this.className = "hidden"; }
11+
};
1312

1413
QUnit.module( "effects", {
15-
setup: function() {
16-
window.requestAnimationFrame = null;
14+
beforeEach: function() {
1715
this.sandbox = sinon.sandbox.create();
1816
this.clock = this.sandbox.useFakeTimers( 505877050 );
17+
1918
this._oldInterval = jQuery.fx.interval;
2019
jQuery.fx.step = {};
2120
jQuery.fx.interval = 10;
21+
22+
// Simulate rAF using the mocked setTimeout as otherwise we couldn't test
23+
// the rAF + performance.now code path.
24+
// Support: IE <10, Android <4.4, Node with jsdom 9.4
25+
if ( window.requestAnimationFrame ) {
26+
this.sandbox.stub( window, "requestAnimationFrame", function( callback ) {
27+
return window.setTimeout( callback, jQuery.fx.interval, Date.now() - 99989.6394 );
28+
} );
29+
}
30+
31+
// Fake performance.now() returning lower values than Date.now()
32+
// and that its values are fractional.
33+
// Support: IE <10, Safari <8.0, iOS <9, Android <4.4, Node with jsdom 9.4
34+
if ( window.performance && typeof window.performance.now === "function" ) {
35+
this.sandbox.stub( window.performance, "now", function() {
36+
return Date.now() - 99999.6394;
37+
} );
38+
}
39+
40+
this.sandbox.stub( jQuery, "now", Date.now );
2241
},
23-
teardown: function() {
42+
afterEach: function() {
2443
this.sandbox.restore();
2544
jQuery.fx.stop();
2645
jQuery.fx.interval = this._oldInterval;
27-
window.requestAnimationFrame = oldRaf;
2846
return moduleTeardown.apply( this, arguments );
2947
}
30-
} );
48+
}, function() {
3149

3250
QUnit[ jQuery.find.compile ? "test" : "skip" ]( "sanity check", function( assert ) {
3351
assert.expect( 1 );
@@ -2629,4 +2647,21 @@ QUnit.test( "jQuery.speed() - durations", function( assert ) {
26292647
jQuery.fx.off = false;
26302648
} );
26312649

2650+
} );
2651+
2652+
QUnit.module( "effects-nostubs", function() {
2653+
2654+
testIframe(
2655+
"matching timestamp",
2656+
"effects/matchingTimestamps.html",
2657+
function( assert, framejQuery, frameWin, frameDoc, success, failureReason ) {
2658+
assert.expect( 1 );
2659+
2660+
assert.ok( success, failureReason );
2661+
}
2662+
);
2663+
2664+
} );
2665+
26322666
} )();
2667+

test/unit/support.js

Lines changed: 26 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,8 @@ testIframe(
7373
"pixelPosition": true,
7474
"radioValue": true,
7575
"reliableMarginLeft": true,
76-
"scrollboxSize": true
76+
"scrollboxSize": true,
77+
"smoothAnim": true
7778
},
7879
ie_10_11: {
7980
"ajax": true,
@@ -90,7 +91,8 @@ testIframe(
9091
"pixelPosition": true,
9192
"radioValue": false,
9293
"reliableMarginLeft": true,
93-
"scrollboxSize": true
94+
"scrollboxSize": true,
95+
"smoothAnim": true
9496
},
9597
ie_9: {
9698
"ajax": true,
@@ -107,7 +109,8 @@ testIframe(
107109
"pixelPosition": true,
108110
"radioValue": false,
109111
"reliableMarginLeft": true,
110-
"scrollboxSize": false
112+
"scrollboxSize": false,
113+
"smoothAnim": false
111114
},
112115
chrome: {
113116
"ajax": true,
@@ -124,7 +127,8 @@ testIframe(
124127
"pixelPosition": true,
125128
"radioValue": true,
126129
"reliableMarginLeft": true,
127-
"scrollboxSize": true
130+
"scrollboxSize": true,
131+
"smoothAnim": true
128132
},
129133
safari: {
130134
"ajax": true,
@@ -141,7 +145,8 @@ testIframe(
141145
"pixelPosition": true,
142146
"radioValue": true,
143147
"reliableMarginLeft": true,
144-
"scrollboxSize": true
148+
"scrollboxSize": true,
149+
"smoothAnim": true
145150
},
146151
safari_9_10: {
147152
"ajax": true,
@@ -158,7 +163,8 @@ testIframe(
158163
"pixelPosition": false,
159164
"radioValue": true,
160165
"reliableMarginLeft": true,
161-
"scrollboxSize": true
166+
"scrollboxSize": true,
167+
"smoothAnim": true
162168
},
163169
firefox: {
164170
"ajax": true,
@@ -175,7 +181,8 @@ testIframe(
175181
"pixelPosition": true,
176182
"radioValue": true,
177183
"reliableMarginLeft": true,
178-
"scrollboxSize": true
184+
"scrollboxSize": true,
185+
"smoothAnim": true
179186
},
180187
firefox_60: {
181188
"ajax": true,
@@ -192,7 +199,8 @@ testIframe(
192199
"pixelPosition": true,
193200
"radioValue": true,
194201
"reliableMarginLeft": false,
195-
"scrollboxSize": true
202+
"scrollboxSize": true,
203+
"smoothAnim": true
196204
},
197205
ios: {
198206
"ajax": true,
@@ -209,7 +217,8 @@ testIframe(
209217
"pixelPosition": true,
210218
"radioValue": true,
211219
"reliableMarginLeft": true,
212-
"scrollboxSize": true
220+
"scrollboxSize": true,
221+
"smoothAnim": true
213222
},
214223
ios_9_10: {
215224
"ajax": true,
@@ -226,7 +235,8 @@ testIframe(
226235
"pixelPosition": false,
227236
"radioValue": true,
228237
"reliableMarginLeft": true,
229-
"scrollboxSize": true
238+
"scrollboxSize": true,
239+
"smoothAnim": true
230240
},
231241
ios_8: {
232242
"ajax": true,
@@ -243,7 +253,8 @@ testIframe(
243253
"pixelPosition": false,
244254
"radioValue": true,
245255
"reliableMarginLeft": true,
246-
"scrollboxSize": true
256+
"scrollboxSize": true,
257+
"smoothAnim": false
247258
},
248259
ios_7: {
249260
"ajax": true,
@@ -260,7 +271,8 @@ testIframe(
260271
"pixelPosition": false,
261272
"radioValue": true,
262273
"reliableMarginLeft": true,
263-
"scrollboxSize": true
274+
"scrollboxSize": true,
275+
"smoothAnim": false
264276
},
265277
android: {
266278
"ajax": true,
@@ -277,7 +289,8 @@ testIframe(
277289
"pixelPosition": false,
278290
"radioValue": true,
279291
"reliableMarginLeft": false,
280-
"scrollboxSize": true
292+
"scrollboxSize": true,
293+
"smoothAnim": false
281294
}
282295
};
283296

test/unit/tween.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,11 @@ if ( !jQuery.fx ) {
55
return;
66
}
77

8-
var oldRaf = window.requestAnimationFrame;
8+
var oldSmoothAnim = jQuery.support.smoothAnim;
99

1010
QUnit.module( "tween", {
1111
setup: function() {
12-
window.requestAnimationFrame = null;
12+
jQuery.support.smoothAnim = false;
1313
this.sandbox = sinon.sandbox.create();
1414
this.clock = this.sandbox.useFakeTimers( 505877050 );
1515
this._oldInterval = jQuery.fx.interval;
@@ -20,7 +20,7 @@ QUnit.module( "tween", {
2020
this.sandbox.restore();
2121
jQuery.fx.stop();
2222
jQuery.fx.interval = this._oldInterval;
23-
window.requestAnimationFrame = oldRaf;
23+
jQuery.support.smoothAnim = oldSmoothAnim;
2424
return moduleTeardown.apply( this, arguments );
2525
}
2626
} );

0 commit comments

Comments
 (0)