2

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?

Sebastian Simon
  • 18,263
  • 7
  • 55
  • 75
User9728
  • 59
  • 5
  • 2
    Well, `"Hello"` isn’t a variable that changes every frame. – Sebastian Simon Feb 26 '22 at 01:10
  • 5
    The arrow function will use the "latest version" of `rAf`, whereas the non-arrow function will use `rAf` when you called the `setTimeout()` method – Nick Parsons Feb 26 '22 at 01:11
  • @NickParsons Could you explain it in more detail? Is it because the non-arrow function is use the parameters' values when calling the `setTimeout()`, whereas the arrow function uses them when executed after the timer is over? – User9728 Feb 26 '22 at 02:29
  • 2
    @User9728 yes, the arrow function will only be invoked after ~1000ms, and by that time, your `rAf` would refer to the newest `rAf` (you haven't shown it here, but I'm assuming your calling `rAf = requestAnimationFrame(draw)` inside of the `draw` function - creating [mre] would help clarify this). With the non-arrow function, you're canceling the `rAf` that is set at the time the line `setTimeout()` is executed (and not the one that is set after the ~1000ms) – Nick Parsons Feb 26 '22 at 03:44
  • 3
    Besides the explanation that other users already provided, there is not really any good reason to use the variadic version of `setTimeout`. It has been kept for compatibility reasons, but I'd consider it a bad practice – Christian Vincenzo Traina Feb 26 '22 at 16:08

1 Answers1

2

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.

Sebastian Simon
  • 18,263
  • 7
  • 55
  • 75