5

My goal is a slideshow of background images with HTML/CSS/JS. Many solutions that I've found promote something like this:

my_recursion();

function my_recursion () {
 // cycle the Background image ...
 setTimeout(my_recursion, 3000);
}

Am I wrong to assume that this is bad style? I would expect that at e.g. cycle 1000 all the other 999 instances of my_recursion are still open / on the stack? Doesn't this create and infinite stack which consumes more and more memory?

Or is there some sort of intelligence involved which does something like "if a function calls itself at the end, the (n-1)th function is destroyed including all variables that were assigned inside of it"?

Robert
  • 432
  • 1
  • 4
  • 15
  • 2
    There is only one entry of `my_recursion` on the stack ever. The first execution finishes completely before the second one is launched. – VLAZ Jan 30 '19 at 15:17
  • Probably not exactly a dupe, [but I've written before about call stack, recursion, and `setTimeout` (as a mechanism to interact with the queue) before](https://stackoverflow.com/questions/39459236/understanding-event-queue-and-call-stack-in-javascript/39459913#39459913) – VLAZ Jan 30 '19 at 15:18
  • But why? Does that mean a `var x = 1` right after my `setTimeout` would never be executed? – Robert Jan 30 '19 at 15:20
  • 1
    @Robert the calls to `setTimeout()` return immediately. The system keeps track of the pending timer and calls the callback function when the time comes. – Pointy Jan 30 '19 at 15:20
  • @Robert it *would* be executed but `setTimeout` will schedule the next execution *after* the current one ends by placing it on the queue. – VLAZ Jan 30 '19 at 15:21
  • Okay I see. `setTimeout` returns immediately. I thought it would wait 3 seconds, then call the function, and then return to the original function. But what if after 3 seconds my original function is still running because there are complicated calculations following the `setTimeout` call? I've posted that same question under the answer below. – Robert Jan 30 '19 at 15:31
  • 2
    @Robert no, it's not possible. I urge you to have a look at the other question I linked and potentially look into the event queue more. If your function is still running in 3s then *nothing else would be running*. Only when it finishes, any other scheduled code would run - you won't get two parallel executions. – VLAZ Jan 30 '19 at 15:33
  • Okay thank you, that makes sense. [This](https://softwareengineering.stackexchange.com/a/314775) answer also helped. Everything is pushed onto the queue and executed at the scheduled time IF everything that was scheduled before it has finished. Thus it's a MINIMUM execution delay as mentioned in the linked answer. So it's not bad practice :) Thank you! – Robert Jan 30 '19 at 15:41
  • @Robert exactly - the key thing here is understanding the queue. The delay you add for `setTimeout` is actually more of a *suggestion* and the actual delay you get may differ . – VLAZ Jan 30 '19 at 15:56
  • Also, the function is not called recursively, rather it's called repetitively. The result would be the same as calling the function multiple times sequentially (without a delay in this case ofcourse). But in your case, the calling sequence is endless, unless you add a condition before `setTimeout`, or clear the timeout somewhere (which currently is not possible, since the `id` of the timeout is not stored anywhere). – Teemu Jan 30 '19 at 16:07

4 Answers4

5

This will not result in endless stack increase, because of the way setTimeout works, and imho it is not bad style.

setTimeout does not guarantee that code will run directly after the given timeout. Instead, after that timeout it will push the callback onto a "queue", which will be processed when the stack is empty. So it will only run when my_recursion has returned and the stack is empty.

If a function calls itself at the end (...)

my_recursion doesn't call itself anywhere. It just passes itself as an argument to setTimeout. After that, it will just continue executing, return directly after, and will be popped from the stack.

This presentation explains the stack and the event queue.

Rengers
  • 14,911
  • 1
  • 36
  • 54
  • Does that mean, IF I had code right after `setTimeout` which took longer than 3 seconds to execute (e.g. 4 seconds), the next function would start to execute in parallel while my (n-1)th instance is still running? Or does that mean that the next instance starts after 4 seconds because it was unable to start after 3 since the (n-1)th function was still running? – Robert Jan 30 '19 at 15:27
  • I don't think there is a requirement for the function to call itself *immediately* and *explicitly* for it to be recursive. As long as it results in another execution of the same function, it should be recursive. If only direct and explicit calls were the requirement, then tail-call optimisation (TCO) would matter when considering if a function is recursive or not. – VLAZ Jan 30 '19 at 15:36
  • @Robert: No, the code would not execute in parallel. `setTimeout` does not guarantee that code will run directly after the given timeout. Instead, after that timeout it will push the callback onto a "queue", which will be processed when the stack is empty. So it will only run when `my_recursion` has returned and the stack is empty. – Rengers Jan 30 '19 at 16:38
  • I've added a link to a presentation at JSConf that explains this. – Rengers Jan 30 '19 at 16:38
  • @vlaz: You are right, I've updated my answer to more accurate. – Rengers Jan 30 '19 at 16:41
2

In your question, your function does not have any parameters. In a real implementation, I hope you plan to use them.

const cycleBackground = (elem, bgs = [], ms = 1e3, i = 0) =>
  ( elem.setAttribute ('style', bgs[i])
  , setTimeout
      ( cycleBackground      // function to schedule
      , ms                   // when to schedule, ms from now
      , elem                 // user-specified element to change
      , bgs                  // user-specified backgrounds
      , ms                   // user-specified delay
      , (i + 1) % bgs.length // next background index
      )
  )

const backgrounds =
  [ "background-color: red;"
  , "background-image: linear-gradient(45deg, cyan 0%, purple 75%);"
  , "background-color: green;"
  ]

// call site
cycleBackground
  ( document.body // element to target
  , backgrounds   // list of backgrounds
  , 3e3           // delay, 3 seconds
  )
p {
  text-align: center;
  font-size: 3vw;
  font-weight: bold;
  color: white;
}
<p>Wait 3 seconds...</p>
Mulan
  • 129,518
  • 31
  • 228
  • 259
0

The code is fine. It destroys all the variables because when you call it first time. It setTimeout() for the next function and at last return. You function doesnot return the the next.

my_recursion();

function my_recursion () {
 // cycle the Background image ...
 setTimeout(my_recursion, 3000); //Sets timeout for next function.
 //returns undefined here
} 
Maheer Ali
  • 35,834
  • 5
  • 42
  • 73
0

Adding to https://stackoverflow.com/a/54443904/11022136. Wanted to give some evidence. Ran the following on node 14.

test.js:

let i = 10;
const canThisOverflow = () => {
    i--;
    console.trace();
    if (i > 0) setTimeout(canThisOverflow, 1);
}
canThisOverflow();

Output: Stack size does not increase

Trace
    at canThisOverflow (/Users/arjunmalik/Shipsy/query-builder/test.js:4:10)
    at Object.<anonymous> (/Users/arjunmalik/Shipsy/query-builder/test.js:7:1)
    at Module._compile (internal/modules/cjs/loader.js:1063:30)
    at Object.Module._extensions..js (internal/modules/cjs/loader.js:1092:10)
    at Module.load (internal/modules/cjs/loader.js:928:32)
    at Function.Module._load (internal/modules/cjs/loader.js:769:14)
    at Function.executeUserEntryPoint [as runMain] (internal/modules/run_main.js:72:12)
    at internal/main/run_main_module.js:17:47
Trace
    at Timeout.canThisOverflow [as _onTimeout] (/Users/arjunmalik/Shipsy/query-builder/test.js:4:10)
    at listOnTimeout (internal/timers.js:554:17)
    at processTimers (internal/timers.js:497:7)
Trace
    at Timeout.canThisOverflow [as _onTimeout] (/Users/arjunmalik/Shipsy/query-builder/test.js:4:10)
    at listOnTimeout (internal/timers.js:554:17)
    at processTimers (internal/timers.js:497:7)

test2.js:

let i = 10;
const canThisOverflow = () => {
    i--;
    console.trace();
    if (i > 0) canThisOverflow();
}
canThisOverflow();

Output: Stack size increases

Trace
    at canThisOverflow (/Users/arjunmalik/Shipsy/query-builder/test2.js:4:10)
    at Object.<anonymous> (/Users/arjunmalik/Shipsy/query-builder/test2.js:7:1)
    at Module._compile (internal/modules/cjs/loader.js:1063:30)
    at Object.Module._extensions..js (internal/modules/cjs/loader.js:1092:10)
    at Module.load (internal/modules/cjs/loader.js:928:32)
    at Function.Module._load (internal/modules/cjs/loader.js:769:14)
    at Function.executeUserEntryPoint [as runMain] (internal/modules/run_main.js:72:12)
    at internal/main/run_main_module.js:17:47
Trace
    at canThisOverflow (/Users/arjunmalik/Shipsy/query-builder/test2.js:4:10)
    at canThisOverflow (/Users/arjunmalik/Shipsy/query-builder/test2.js:5:13)
    at Object.<anonymous> (/Users/arjunmalik/Shipsy/query-builder/test2.js:7:1)
    at Module._compile (internal/modules/cjs/loader.js:1063:30)
    at Object.Module._extensions..js (internal/modules/cjs/loader.js:1092:10)
    at Module.load (internal/modules/cjs/loader.js:928:32)
    at Function.Module._load (internal/modules/cjs/loader.js:769:14)
    at Function.executeUserEntryPoint [as runMain] (internal/modules/run_main.js:72:12)
    at internal/main/run_main_module.js:17:47
Trace
    at canThisOverflow (/Users/arjunmalik/Shipsy/query-builder/test2.js:4:10)
    at canThisOverflow (/Users/arjunmalik/Shipsy/query-builder/test2.js:5:13)
    at canThisOverflow (/Users/arjunmalik/Shipsy/query-builder/test2.js:5:13)
    at Object.<anonymous> (/Users/arjunmalik/Shipsy/query-builder/test2.js:7:1)
    at Module._compile (internal/modules/cjs/loader.js:1063:30)
    at Object.Module._extensions..js (internal/modules/cjs/loader.js:1092:10)
    at Module.load (internal/modules/cjs/loader.js:928:32)
    at Function.Module._load (internal/modules/cjs/loader.js:769:14)
    at Function.executeUserEntryPoint [as runMain] (internal/modules/run_main.js:72:12)
    at internal/main/run_main_module.js:17:47
Arjun Malik
  • 421
  • 4
  • 5