Home > OS >  Why do I have to use an anonymous function instead of passing an additional argument to `setTimeout`
Why do I have to use an anonymous function instead of passing an additional argument to `setTimeout`

Time:02-27

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.

  • Related