I want to stop this requestAnimationFrame
after 1 second:
rAF = requestAnimationFrame(draw);
So I use setTimeout
.
When I wrap the cancelAnimationFrame(rAF)
in an arrow function, it works fine:
setTimeout(() => cancelAnimationFrame(rAF), 1000);
But, when I use cancelAnimationFrame
as the function itself and pass rAF
as the third argument to setTimeout
, it doesn’t work:
setTimeout(cancelAnimationFrame, 1000, rAF);
I thought that I didn’t know the exact syntax for setTimeout
at first. But, I think the syntax isn’t wrong as this code works fine:
setTimeout(alert, 1000, "Hello");
Why doesn’t it work?
CodePudding user response:
The difference is the moment in time at which rAF
is evaluated.
Explanation
Presumably, your code looks something like this:
let rAF;
const draw = () => {
// Do some work.
rAF = requestAnimationFrame(draw);
};
rAF = requestAnimationFrame(draw);
When you do this:
setTimeout(cancelAnimationFrame, 1000, rAF);
you pass three values to setTimeout
: a function, a number, and another number.
When this statement is executed the call is evaluated, meaning that first, the setTimeout
identifier is resolved to a function reference.
Second, the three arguments are evaluated: the cancelAnimationFrame
identifier is resolved to a function reference, then the number literal is a number primitive, then the rAF
identifier is resolved to another number primitive.
Then, the call is performed.
That’s all that setTimeout
sees.
In JavaScript, you cannot pass a reference to a number, like you can in C, for example.
Let’s assume rAF
is initially 1
.
Over the course of one second, rAF
has been repeatedly incremented and eventually reaches the value 61
or so.
Since you register the setTimeout
at the start, the statement
setTimeout(cancelAnimationFrame, 1000, rAF);
is equivalent to
setTimeout(cancelAnimationFrame, 1000, 1);
However, the statement
setTimeout(() => cancelAnimationFrame(rAF), 1000);
is not equivalent to
setTimeout(() => cancelAnimationFrame(1), 1000);
The function bodies are only evaluated when they are called. This means, JS doesn’t “peek inside the functions” and attempt to evaluate variables. That statement essentially means “call some function with some other function and the number 1000 as arguments”.
When the one second is over and it’s time to cancel the animation frame, setTimeout
executes its callback.
If the callback is () => cancelAnimationFrame(rAF)
, then it’s executed, so the function body is evaluated: cancelAnimationFrame(rAF)
is equivalent to cancelAnimationFrame(61)
.
However, in the non-working case, cancelAnimationFrame
stays the same, the argument 1
(equivalent to rAF
at the time setTimeout
was originally called) stays the same.
You can’t cancel frame 1 when you’re already at frame 61.
And setTimeout(alert, 1000, "Hello");
works, of course, because "Hello"
is static, is only evaluated once, never changes.
Related
Here’s a more general situation where this behavior can be examined:
let greeting = "Hello";
const greet = (theGreeting) => console.log(`${theGreeting}, world!`);
const boundGreeting = greet.bind(null, greeting);
greeting = "Goodbye";
boundGreeting(); // Logs "Hello, world!".
greet(greeting); // Logs "Goodbye, world!".
bind
passes the 2nd parameter (greeting
) as the 1st argument to greet
(ignore the null
).
This is much like using setTimeout(greet, 0, greeting);
, except this is unrelated to timeouts and we call (the bound) greet
ourselves.
You could pass something like a reference, i.e. an object, to make it work, if you had a function that also accepts an object:
const timing = {
rAF: null
},
cancelTiming = ({ rAF }) => cancelAnimationFrame(rAF),
draw = () => {
// Do some work.
timing.rAF = requestAnimationFrame(draw);
};
timing.rAF = requestAnimationFrame(draw);
setTimeout(cancelTiming, 1000, timing);
This works because when passing timing
, the value being passed is a reference.
Mutating it, e.g. by updating the rAF
property, is visible everywhere where this reference is visible.
But this makes programming quite cumbersome.
This is tangentially related to Is JavaScript a pass-by-reference or pass-by-value language?.
Alternative
There’s an alternative to using setTimeout
.
When requestAnimationFrame
calls its callback function, it passes a DOMHighResTimeStamp
, similar to what performance.now
returns.
So you could make the check in your draw
function:
const timeoutTimestamp = performance.now() 1000,
draw = (now) => {
// Do some work.
if(now < timeoutTimestamp){
requestAnimationFrame(draw);
}
};
requestAnimationFrame(draw);
Related: Stop requestAnimationFrame after a couple of seconds.