Why doesn't my setTimeout callback not get called? - javascript

I'm busy building a flash-card game. I want to give the user a visible countdown before a card get's flashed on screen. My script for the countdown looks like this:
let downSeconds = 5;
while (downSeconds > 0) {
setTimeout(function() {
$("#timerDisplay").text = downSeconds;
downSeconds--;
}, 1000);
}
$(".detail-card").removeClass("hidden");
If I didn't want the updated seconds I'd just use a 5000ms 'setTimeOut'. I did before try with a setInterval, with a delay of 1000ms, so every time it elapses it updates the seconds.
Now, if I put a breakpoint on either line of the setTimeOut callback, and only there, nothing happens when the setTimeout is invoked, so the seconds display never updates, and I'm in an infinite loop, because downSeconds--; is never invoked, so downSeconds keeps the value of 5 all throughout.
What am I doing wrong?

setTimeout runs the code later, while the while loop runs "now". You can't successfully combine the two.. So, you have to write your code differently, something like this should work:
let downSeconds = 5;
function doCountDown() {
downSeconds--;
$("#timerDisplay").text = downSeconds;
if (downSeconds > 0) {
setTimeout(doCountDown, 1000);
} else {
$(".detail-card").removeClass("hidden");
}
}
setTimeout(doCountDown, 1000);

You may use ES7 and await the loop for a second:
const time = ms => new Promise(res => setTimeout(res,ms));
(async function(){
let downSeconds = 5;
while (downSeconds > 0) {
await time(1000);
$("#timerDisplay").text = downSeconds;
downSeconds--;
}
$(".detail-card").removeClass("hidden");
})()

The setTimeout call is asynchronous,
and so by the time the 1000 milliseconds scheduled by the first call to setTimeout elapses,
the while-loop will have executed its body thousands of times,
each time scheduling a new job with setTimeout,
causing massive scheduling work to the JavaScript engine.
The engine is too busy to execute the function,
and it just keeps getting worse,
as the loop keeps running and keeps scheduling more and more.
I would expect your execution environment to become unresponsive and unable to make progress, unable to actually call the first function scheduled.
Use setInterval and clearInterval instead, for example:
let counter = 5;
let interval = setInterval(() => {
$("#timerDisplay").text = counter;
counter--;
if (counter == 0) {
clearInterval(interval);
$(".detail-card").removeClass("hidden");
}
}, 1000);

Related

Is there a way to alternate setInterval durations?

I want to be able to call setInterval (or something similar) at two different lengths, alternating.
For example, running a function after 5 seconds, then 1 second, then 5 seconds again, and so on.
Is this possible? I tried a function that alternates the value, but it didn't seem to work.
let num = 5000
function alternateNum() {
if (num === 5000) { num = 1000 }
else { num = 5000 }
}
setInterval(() => {
// ...
alternateNum()
}, num);
JS timers have a very complicated history.
Using a recursive setTimeout invocation is a simple and elegant solution as long (as your runtime implements tail call optimization).
Separate from the issue of recursion is the issue of timer drift. This is covered in the YouTube video JavaScript counters the hard way - HTTP 203 if you'd like an accessible introduction.
In many JS engines (e.g. V8) setInterval will handle drift correction for you, so there's actually an advantage to using it over recursively invoking setTimeout. (Check the millisecond timestamps in the console messages in the snippet below to verify this.)
In order to determine the constant interval argument you'll need for setInterval, you'll need to find the greatest common factor of your delay durations. Once you have this value, you can use it as the base interval delay, and keep track of your interval state to determine whether you should switch to the next interval delay, run your other code, etc. Here's a minimal example:
const durations = [1000, 5000];
// If you can't determine this in advance and use a constant value,
// then you can calculate it at runtime using a function:
const gcf = 1000; // or const gcf = findGreatestCommonFactor(durations);
let durationIndex = 0;
let elapsed = 0;
function update () {
elapsed += gcf;
const ready = elapsed === durations[durationIndex];
if (ready) {
elapsed = 0;
durationIndex = (durationIndex + 1) % durations.length;
}
return ready;
}
setInterval(() => {
const ready = update();
if (!ready) return;
// Do your interval task, for example:
console.log('tick');
}, gcf);
The problem with setInterval() is that the time is taken into account just once. You can use setTimeout() with recursion instead:
function doAction(flipFlop) {
setTimeout(() => {
console.log(flipFlop ? 'flip' : 'flop');
doAction(!flipFlop);
// do some other action...
}, flipFlop ? 1000 : 3000);
}
doAction(true);
Watch out though if you have a long running process, this recursion gets deeper and deeper.
I think this method is the easiest:
setInterval(() => {
console.log("first");
setTimeout(() => console.log("second"), 750);
}, 2000);
This creates an interval that alternates between 1250 and 750 milliseconds.
The problem with your code
let num = 5000
function alternateNum() {
if (num === 5000) { num = 1000 }
else { num === 5000 }
}
setInterval(() => {
// ...
alternateNum()
}, num);
The last few lines (the setInterval) call are only getting called once with the initial value of num and thus any future changes to num won't be reflected in the setTimeout call.
How to fix it
You should use setTimeout within the function that has your code and call your function recursively:
const doStuff = (time = 1000) => {
// Your code here
// generate the next time to wait
const nextTime = time === 5000 ? 1000 : 5000;
// call the function again after waiting `time` milliseconds
setInterval(() => doStuff(nextTime), time);
}
Then you would call doStuff to start it. If you wanted to start it immediately with the next one happening after 1 second you could do:
doStuff();
Or if you wanted to call it after 5 seconds with the next one happening one second after that:
setTimeout(doStuff, 5000);
The difference here compared to your code is that the variable that represents the time is being used over and over again as it changes instead of just once on initial code execution.

how create a continuous loop that calls a function using a random generated interval

I want to execute a function every specified seconds, and it should loop forever. When the function is finished I want to start a new setTimeout using a random generated value between 2 and 5 (represents seconds).
Maybe badly explained but..
this is what I have so far.
function Start() {
let count = $("section.mosaic").find("a.item").length;
ChangePic();
setTimeout(function () {
let interval = CREATOR.PUB.Utility.randomInterval(2, 5);
console.log(interval);
}, 3000);
function ChangePic() {
}
}
Move the interval code into ChangePic so that it can call itself when it finishes.
function Start() {
let count = $("section.mosaic").find("a.item").length;
ChangePic();
function ChangePic() {
// do stuff here
let interval = CREATOR.PUB.Utility.randomInterval(2, 5);
setTimeout(ChangePic, interval * 1000);
}
}
Note that this will call ChangePic once immediately when Start runs, and the random intervals will begin thereafter. If you need the initial execution to be on a delay as well, you could either copy the timeout code and run it instead of calling ChangePic();, or move it into a helper function that you call both inside Start and ChangePic.

keep calling a function until a criteria is hit

writing a simple blackjack game
I want it to be semi automated.
I have this function after the player hits stick
stick = () => {
while(this.dealersTotal() < 21){
setInterval(this.dealToDealer, 3000)
}
}
however, when I do this it just freezes as I think im exceeding the stack. any ideas why this would do this? I can post more code but not sure seeing the other functions will help too much
dealersTotal is just a function that returns a number
dealToDealer just sets the state of dealers hand with the next card and then removes the top card from the deck
The function setInterval will be executed recurrently unless you stop it with the function clearInterval.
Use the setTimeout instead:
stick = () => {
while(this.dealersTotal() < 21){
setTimeout(this.dealToDealer, 3000);
}
}
Important: If this.dealersTotal() always returns a number less than 21 your code will enter in an infinite loop.
While loop can sick your perfomance, try to play around with using only interval.
Example:
stick = () => {
this.interval = setInterval(() => {
if(this.dealersTotal() < 21) {
this.dealersTotal();
clearInterval(this.interval)
}
}, 3000);
}
You have an infinite loop that is creating new intervals (which will run indefinitely each) on each iteration. That's what's killing your app.
The simplest solution would be (https://jsfiddle.net/29L556s9/10/):
stick = () => {
setTimeout(() => {
if(dealersTotal() < 21) {
dealToDealer();
stick();
}
}, 3000);
};
`Let interval = setInterval(()=>{
//Do what you want here
If(this.dealersTotal() >= 21){ clearInterval(interval) } // keep interval loop running until dealersTotal is bigger than or equal to 21
},50)//50ms`
With your current one, you are calling while loop and this loop will create a new setInterval so if you have 10 while loop, setInterval will be called 10 times which will create infinite interval which eventually exceeds processing power. So the best is to keep interval only as while loop will block. While loop is good for definite value or until something happens such as a keyboard press. But when you want a UI multiple things happening then setInterval is the better option.

setInterval in function keeps speeding and clear interval is ignored

When I activate showMoves() the setInterval is supposed to repeat the function one second at a time.
However, it speeds up after a few seconds and activates the function several times in a single second instead of once. Also the clearInterval isn't working even though the if statement for it turns true.
let i = -1;
function showMoves() {
const start = setInterval(showMoves, 1000);
if (i > game.computerMoves.length) {
clearInterval(start);
}
console.log(i + ' ' + game.computerMoves.length);
const showColors = new Map([
[green, 'lime'],
[yellow, 'rgb(255,255,102)'],
[blue, 'dodgerblue'],
[red, 'salmon'],
]);
i++;
let move = game.computerMoves[i];
move.style.backgroundColor = showColors.get(move);
}
You shouldn't be calling setInterval inside the very same function that's being called by setInterval -- you're setting up multiple interval timers that way, not just one that you can clear away easily. It's "speeding up" as you say because you're seeing more and more different interval timers calling the same function.
You're recursively calling showMoves - every second, a new interval is created. When the function runs and the stop condition isn't reached, the start reference you have to the interval you just created is just garbage collected. You want a method to save the intervals so that you can reference them later so they can be cleared. For example:
let intervals = [];
function showMoves() {
intervals.push(setInterval(showMoves, 1000));
if (i > game.computerMoves.length) {
// clear all currently running intervals:
intervals.forEach(clearInterval);
intervals = [];
}
// ...

Can I stop the execution of a function from outside that function?

I have this code:
function toStop(){
while(true){}
}
toStop();
Now, how can I stop this? Or how can I kill the current thread if this function call is somewhere in the setInterval running thread? Example:
var id = setInterval(function(){
toStop();
}, 1000);
//stop thread/timer with id here.
clearInterval doesn't work because it waits until the function call ends.
Thanks!
"Can I stop the execution of a function from outside that function?"
No, you can't programmatically.
JavaScript is single-threaded and if you run a piece of code that makes it infinitely busy, such as while(true);, then nothing else will ever be able to execute.
Calling such a piece of code within setTimeout or setInterval will have the same result, since the callback of these gets executed in the only thread we have as well.
However, you can create a timed recurring execution using setInterval or setTimeout, which can be stopped.
var timerId = setInterval(function () {
//Process an iteration of the loop in here
//If you cause an infinite loop in here, you will have the same issue
}, 50);
//stop the timer after ~3 seconds
setTimeout(clearInterval.bind(null, timerId), 3000);
Notes:
4 is the lowest interval that could be honored as specified in the SPEC.
setInterval will stack if the callback takes more time to execute than the specified interval. For that reason I never use setInterval and always use setTimeout.
Timer intervals are not guaranteed to be accurate
e.g. with setTimeout
var stopProcessing = startProcessing();
//Stop processing after ~3 seconds
setTimeout(stopProcessing, 3000);
function startProcessing() {
var timerId;
!function process() {
//Do some processing
//Continue processing in ~50 ms
timerId = setTimeout(process, 50);
}();
return function () { clearTimeout(timerId); }
}
Instead of an infinite loop, just use an if statement and wrap it in an interval:
var shouldContinue = true;
var interval = 0;
function toStop() {
if (interval == 0) {
interval = setInterval(function() {
if(shouldContinue) {
...
}
else {
clearInterval(interval);
interval = 0;
}
}, 200); // Or whatever interval makes sense
}
}
toStop();
// ...
shouldContinue = false;
See this principle in action here.
No, you can't programmatically, as #plalx said but you could try this: declaring a binding outside and check on that to continue or stop the loop:
let letMeGoOut;
function toStop(){
while(letMeGoOut != false)
}
toStop();
Here, I've created a function on mouseover that triggers a loop changing the opacity of the h1. It goes on till the mouse cursor moves out and is over something else in the page.
Here is the example: https://codepen.io/Mau-Di-Bert/pen/VqrRxE

Categories