I am making a memory game in javascript that I am putting on my web page. I have 3 buttons labeled 1, 2, and 3, and they are supposed to turn green then white again in a certain order. The order is random and increases by 1 every turn. The player is then supposed to click the buttons in the correct order. The problem is that when I change the button color to green, instead of each button turning green and then white again in the correct order, the buttons turn green all at once. This is the javascript logic:
function lightUp() {
var arrayOrder = gameOrder.split("");
for(let i = 0; i < arrayOrder.length; i++) {
document.getElementById("but" + arrayOrder[i]).style.backgroundColor = "green";
setTimeout(function() {
document.getElementById("but" + arrayOrder[i]).style.backgroundColor = "white";
}, 1000);
}
}
gameOrder is a string of the number order (e.g. "12331232123")
I think the game isn't working because of the way setTimeout works, I believe it pauses in the background and allows the for loop to keep running instead of pausing the whole function for a second (which is what I am trying to do).
I want each button to turn on then off before the next button changes color. So if the order is 1 2 3, I want button 1 to turn green and then white then button 2 to turn green then white and finally for button 3 to turn green then white.
As espacarello has pointed out, code you run with setTimeout is not synchronous. You could make the function asynchronous and await the setTimeout.
This is my preferred way to "animate" things with JS:
async function lightUp() {
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
for (const i of gameOrder.split('')) {
const el = document.getElementById(`but${i}`);
el.style.backgroundColor = 'green';
await sleep(1000);
el.style.backgroundColor = 'white';
}
}
This stays simple and elegant without messying your code with numerous callbacks, recursion, or nested setTimeouts. It stays as one function.
Edit: It is important to note that IE does not support Promises or the Arrow Function syntax. See the other two answers for wider support.
Problem with your code is the for loop runs all at once without a delay. You code it thinking that the interval will cause the loop to wait until it is executed. Problem with that is, will just run without waiting.
Use a queue type of system where you run the step and when it is done, you run the next until you run out of things to do.
var steps = [1,2,3,2,1,3,1]
var delay = 1000
var step = 0
function next () {
var button = document.getElementById('btn' + steps[step])
button.classList.add("on")
window.setTimeout( function () {
button.classList.remove("on")
step++
if(step<steps.length) next()
}, delay)
}
next()
.on {
background-color: green;
}
<button id="btn1">One</button>
<button id="btn2">Two</button>
<button id="btn3">Three</button>
you can do it as below...
function lightUp() {
var arrayOrder = gameOrder.split("");
for (let i = 0; i < arrayOrder.length; i++) {
(function(i) {
setTimeout(function() {
document.getElementById("but" + arrayOrder[i]).style.backgroundColor = "green";
}, 1000 * i - 1000);
setTimeout(function() {
document.getElementById("but" + arrayOrder[i]).style.backgroundColor = "white";
}, 1000 * i); // schedules excution increasingly for each iteration
})(i);
}
}
to learn more about how it works please refere to this answer
Related
in Js, I want to try to simulate a die roll by showing images of die 1 to 6, but when I try to display these in a for loop, it only displays image dice6. I tried putting in a nested for loop to slow down the outer loop but that didnt work. Does the page need to refresh after changing "src" attribute?
const dieImage = function (num) {
return "images/dice" + String(num).trim() + ".png";
};
function dieRoll(num) {
for (let i = 1; i < 7; i++) {
for (let z = 0; z < 44444; z++) {} // attempt to slow
if (num === 1) {
img1.setAttribute("src", dieImage(i));
} else {
img2.setAttribute("src", dieImage(i));
}
}
}
As mentioned in the comments you can use setTimeout. You can introduce a delay and give the browser a chance to redraw by using setTimeout, promise, and await, for example like this:
const DELAY = 300; // 300ms
async function dieRoll(num) {
for (let i = 1; i < 7; i++) {
if (num === 1) {
img1.setAttribute("src", dieImage(i));
} else {
img2.setAttribute("src", dieImage(i));
}
await new Promise((resolve) => setTimeout(() => resolve(), DELAY));
}
}
The loop execution will stop until the promise is resolved, but it will let the browser to continue to respond and redraw. When the promise is resolved after the timeout callback is run after the given DELAY milliseconds, the next iteration of the loop will take place.
What you are missing (roughly) is that the browser paints the screen when the JavaScript code has finished running. Even though you are setting the src attribute to a different image in a loop, the JavaScript code finishes only when the loop ends. The browser paints only once, i.e. the last image you set in the loop. This explanation may be oversimplified, but it gives you an idea.
The solution is to return from the JavaScript code after setting the src and repeating after a suitable delay, giving the user the opportunity to sense the change. setTimeout is probably good enough for your case; in other use cases where you want really smooth animation, there would be other solutions (e.g. requestAnimationFrame()). An untested implementation to demonstrate the intent:
function dieRoll(selectedNum) {
var counter = 8; // how many times to change the die
function repeat() {
if (counter === 1) {
// on the last iteration, set the image representing the selected number
img1.setAttribute("src", dieImage(selectedNum));
} else {
// else decrement the counter, set a random image and repeat after a timeout
counter--;
img1.setAttribute("src", dieImage(Math.floor(6 * Math.random()) + 1));
setTimeout(repeat, 300);
}
}
repeat();
}
I'm making a puzzle solving function which uses an array of puzzle pieces in their current shuffled order. Each piece has an id which points to the correct position in the array. I set overlay colors on the pieces that are about to be swapped. I want for there to be a delay between the pieces being colored and then swapped.
function solvePuzzle() {
while (rezolvat == false) // while all pieces are not in correct position
for (var i = 0; i < _piese.length; i++) { // iterates through array of puzzle pieces
if (checkPiesa(i) == false) {
_piesaCurentaDrop = _piese[i];
_piesaCurenta = getPiesaDestinatie(_piesaCurentaDrop.id); // destination piece
_context.save();
_context.globalAlpha = .4;
_context.fillStyle = PUZZLE_HOVER_TINT;
_context.fillRect(_piesaCurentaDrop.xPos, _piesaCurentaDrop.yPos, _latimePiesa, _inaltimePiesa);
_context.fillStyle = PUZZLE_DESTINATION_HOVER_TINT;
_context.fillRect(_piesaCurenta.xPos, _piesaCurenta.yPos, _latimePiesa, _inaltimePiesa);
_context.restore();
// here I want to have some kind of 'sleep' for 2000 ms so you can see the pieces about to be swapped
dropPiece(); // function for swapping puzzle pieces
}
}
}
You can use javascript's setTimeout() functions which will execute the function after specified milliseconds, you can learn more about it here
function solvePuzzle() {
while (rezolvat == false) // while all pieces are not in correct position
for (var i = 0; i < _piese.length; i++) { // iterates through array of puzzle pieces
(function (i) {
setTimeout(function () {
if (checkPiesa(i) == false) {
_piesaCurentaDrop = _piese[i];
_piesaCurenta = getPiesaDestinatie(_piesaCurentaDrop.id); // destination piece
_context.save();
_context.globalAlpha = .4;
_context.fillStyle = PUZZLE_HOVER_TINT;
_context.fillRect(_piesaCurentaDrop.xPos, _piesaCurentaDrop.yPos, _latimePiesa, _inaltimePiesa);
_context.fillStyle = PUZZLE_DESTINATION_HOVER_TINT;
_context.fillRect(_piesaCurenta.xPos, _piesaCurenta.yPos, _latimePiesa, _inaltimePiesa);
_context.restore();
// here I want to have some kind of 'sleep' for 2000 ms so you can see the pieces about to be swapped
// setTimeout in side task execution
setTimeout(() => dropPiece(), 1000); // function for swapping puzzle pieces
}
}, 2000 * i); // schedules excution increasingly for each iteration
})(i);
}
}
In the code above for loop finishes immediately, however, it schedules the execution of each iteration after a specified increased time(i*2000) using setTimeout
So the execution of the, (Despite for loop's immediate completion)
first iteration will begin immediately at 0*2000=0 mili-seconds,
the task for second execution will be executed after (1*2000),
the task for third execution will be executed after (2*2000),
and so on..
Just for a simple understanding look at the sample code below
Working Code Sample
for (var i = 0; i < 5; i++) {
(function(i) {
setTimeout(function() {
console.log(i);
setTimeout(() => console.log(i + 0.5), 1000); // setTimeout in side task execution in case needed
}, 2000 * i); // schedules excution increasingly for each iteration
})(i);
}
the function should change the background to red and to blue with 1 second
between the two operations, but when i run it i find that the first change does not appear on the page
i put the "sleep" function as i thought the 2 changes happen in the same time
function changeBackGround()
{
document.body.style.backgroundColor = "red";
sleep(1000);
document.body.style.backgroundColor = "blue";
}
function sleep(milliseconds) {
var start = new Date().getTime();
for (var i = 0; i < 1e7; i++)
{
if ((new Date().getTime() - start) > milliseconds)
{
break;
}
}
}
i expect that the page background should first be red and after 1 second it becomes blue
Your homebrewed sleep function locks up the event loop in a busy loop. This prevents the browser from repainting the page, so while the DOM has updated, the background isn't reflected in the rendered page.
Use setTimeout if you want to run a bit of code after some time.
you can define the function to accept the color as an argument like this:
function changeBackGround(color) {
document.body.style.backgroundColor = color;
}
and you can use setTimeout to execute a function after given period in milliseconds
window.onload = function() {
changeBackGround('red');
setTimeout(function() {
changeBackGround('blue');
}, 1000);
}
I did some research about loops and setTimeout function, but still its not working as I wish...
It opens all the links in same time without delay of 5 seconds per link that opened.
I would like it to open every link with 5 second delay after every each of them.
Code:
var links = document.querySelectorAll('a[class="mn-person-info__link ember-view"][id^="ember"]')
for (var i = 1; i <= links.length; i++) {
(function(index) {
setTimeout(function() {
window.open(links[index].href,'_blank');
}, 5000);
})(i);
}
Using a Promise chain and Array#reduce(), you can do this:
var links = document.querySelectorAll('a[class="mn-person-info__link ember-view"][id^="ember"]');
Array.from(links).reduce((chain, { href }) => {
return chain.then(() => new Promise(resolve => {
window.open(href, '_blank');
setTimeout(resolve, 5000);
}));
}, Promise.resolve())
If you don't want to do something quite this fancy and you're fine setting all your timeouts at once, you can simplify your for loop using let instead of var and an IIFE:
var links = document.querySelectorAll('a[class="mn-person-info__link ember-view"][id^="ember"]');
for (let i = 0; i < links.length; i++) {
setTimeout(function() {
window.open(links[i].href, '_blank');
}, 5000 * i);
}
Or even simpler, using for...of and object destructuring:
var links = document.querySelectorAll('a[class="mn-person-info__link ember-view"][id^="ember"]');
var i = 0;
for (const { href } of links) {
setTimeout(function() {
window.open(href, '_blank');
}, 5000 * i++);
}
That's because all your timeouts are set immediately, almost at one time. So the ends of timeouts are take place almost on the same time. Try this:
var links = document.querySelectorAll('a[class="mn-person-info__link ember-view"][id^="ember"]')
for (var i = 1; i <= links.length; i++) {
(function(index) {
setTimeout(function() {
window .open(links[index].href,'_blank');
}, 5000 * i);
})(i);
}
setTimeout is asyncronous. I would solve this specified problem with:
Array.from(links).reduce((a,e)=>{
setTimeout(function() { window .open(e.href,'_blank');}, a);
return a + 5000;
} ,5000);
From the code above i assume that you wish to open the links once every 5 seconds so here are the improvements and suggestion made upon your code
// use JS hoisting for variable declarations
var
// Timer to store the next timer function reference
timer,
// The delay between each function call
delay = 5000,
// Set the desired selector
selectors = 'a[class="mn-person-info__link ember-view"][id^="ember"]',
// Get the list of the required selectors.
links = document.querySelectorAll(selectors);
// Create a function which will be called every X seconds
function openLink( index ){
// validate that the index is not out of bound
if (index === links.length){
return;
}
// get the current link and open new window with the link url
window.open(links[index].href,'_blank');
// Set the next timer to open the next window
timer = setTimeout( openLink, delay, ++index);
}
// Call the function for the first time with index = 0
openLink( 0 );
What does this code do?
The first section is declaration of the variables which will be used in this script. The preferred way to declare variables id to to use hoisting
Hoisting
Hoisting is a JavaScript mechanism where variables and function declarations are moved to the top of their scope before code execution.
Timers
If you wish to open the links in a sequence you should put them inside a function which will call them one after the other instead of using the for loop. The for loop will place all of them in the call stack/event loop and all of them will be executed after 5000 milliseconds since this is what set time out will so, it will schedule the execution of the code to 5000 milliseconds from now for all of them.
Im recommending you to watch the amazing lecture by Philip Roberts
Philip Roberts: What the heck is the event loop anyway
Saving the return value from the setTimeout will allow you to later on cancel the timer if you wish using the clearTimeout( timer )
Summary
Since you had a for loop it will simply loop over all your links. The setTimeout will set the scheduled execution time 5 seconds from now for all the links.
The trick here is to set the next timer after the current one is opened. This is why the sertTimeout is defined within the function itself.
The setTimeout gets a third param which is the parameter passed to the function when its being called.
Easy way:
links.forEach(function(i, link) {
setTimeout(function() {
window.open(link.href,'_blank');
}, 5000 * i);
});
Just wait i * 5 seconds, where i is the index of the link
Suppose you have 3 arrays you want to loop over, with lengths x, y, and z, and for each loop, you want to update a progress bar. For example:
function run() {
x = 100;
y = 100;
z = 10;
count = 0;
for (i=0; i<x; i++) {
//some code
for (j=0; j<y; j++) {
// some code
for (k=0; k<z; k++) {
//some code
$("#progressbar").reportprogress(100*++count/(x*y*z));
}
}
}
}
However, in this example, the progress bar doesn't update until the function completes. Therefore, I believe I need to use setTimeout to make the progress bar update while the function runs, although I'm not sure how to do that when you have nested for loops.
Do I need to break each loop up into its own function, or can I leave them as nested for loops?
I created a jsfiddle page in case you'd like to run the current function: http://jsfiddle.net/jrenfree/6V4Xp/
Thanks!
TL;DR: Use CPS: http://jsfiddle.net/christophercurrie/DHqeR/
The problem with the code in the accepted answer (as of Jun 26 '12) is that it creates a queue of timeout events that don't fire until the triple loop has already exited. You're not actually seeing the progress bar update in real-time, but seeing a late report of what the values of the variables were at the time they were captured in the inner closure.
I'd expect that your 'recursive' solution looks a bit like using continuation-passing style to ensure that your loop doesn't continue until after you've yielded control via setTimeout. You might not know you were using CPS, but if you're using setTimeout to implement a loop, you're probably pretty close to it.
I've spelled out this approach for future reference, because it's useful to know, and the resulting demo performs better than the ones presented. With triple nested loops it looks a bit convoluted, so it may be overkill for your use case, but can be useful in other applications.
(function($){
function run() {
var x = 100,
y = 100,
z = 10,
count = 0;
/*
This helper function implements a for loop using CPS. 'c' is
the continuation that the loop runs after completion. Each
'body' function must take a continuation parameter that it
runs after doing its work; failure to run the continuation
will prevent the loop from completing.
*/
function foreach(init, max, body, c) {
doLoop(init);
function doLoop(i) {
if (i < max) {
body(function(){doLoop(i+1);});
}
else {
c();
}
}
}
/*
Note that each loop body has is own continuation parameter (named 'cx',
'cy', and 'cz', for clarity). Each loop passes the continuation of the
outer loop as the termination continuation for the inner loop.
*/
foreach(0, x, function(cx) {
foreach(0, y, function(cy) {
foreach(0, z, function(cz) {
count += 1;
$('#progressbar').reportprogress((100*(count))/(x*y*z));
if (count * 100 % (x*y*z) === 0) {
/*
This is where the magic happens. It yields
control to the javascript event loop, which calls
the "next step of the foreach" continuation after
allowing UI updates. This is only done every 100
iterations because setTimeout can actually take a lot
longer than the specified 1 ms. Tune the iterations
for your specific use case.
*/
setTimeout(cz, 1);
} else {
cz();
}
}, cy);
}, cx);
}, function () {});
}
$('#start').click(run);
})(jQuery);
You can see on jsFiddle that this version updates quite smoothly.
If you want to use setTimeout you could capture the x, y, z and count variables into a closure:
function run() {
var x = 100,
y = 100,
z = 10,
count = 0;
for (var i=0; i<x; i++) {
for (var j=0; j<y; j++) {
for (var k=0; k<z; k++) {
(function(x, y, z, count) {
window.setTimeout(function() {
$('#progressbar').reportprogress((100*count)/(x*y*z));
}, 100);
})(x, y, z, ++count);
}
}
}
}
Live demo.
Probably a jquery function in reportprogress plugin uses a setTimeout. For example if you use setTimeout and make it run after 0 milliseconds it doesn't mean that this will be run immediately. The script will be executed when no other javascript is executed.
Here you can see that i try to log count when its equal to 0. If i do it in setTimeout callback function then that is executed after all cycles and you will get 100000 no 0. This explains why progress-bar shows only 100%. js Fiddle link to this script
function run() {
x = 100;
y = 100;
z = 10;
count = 0;
for (i=0; i<x; i++) {
//some code
for (j=0; j<y; j++) {
// some code
for (k=0; k<z; k++) {
//some code
if(count===0) {
console.log('log emidiatelly ' + count);
setTimeout(function(){
console.log('log delayed ' + count);
},0);
}
count++;
}
}
}
}
console.log('started');
run();
console.log('finished');
wrapping everything after for(i) in setTimeout callback function made the progress-bar work. js Fiddle link
Edit:
Just checked that style setting code for item is actually executed all the time. I think that it might be a browser priority to execute javascript first and then display CSS changes.
I wrote a another example where i replaced first for loop with a setInterval function. It's a bit wrong to use it like this but maybe you can solve this with this hack.
var i=0;
var interval_i = setInterval(function (){
for (j=0; j<y; j++) {
for (k=0; k<z; k++) {
$("#progressbar").reportprogress(100*++count/(x*y*z));
}
}
i++;
if((i<x)===false) {
clearInterval(interval_i);
}
},0);
JS Fiddle
I've found a solution based on the last reply but changing the interval time to one. This solution show a loader while the main thread is doing an intensive task.
Define this function:
loading = function( runme ) {
$('div.loader').show();
var interval = window.setInterval( function() {
runme.call();
$('div.loader').hide();
window.clearInterval(interval);
}, 1 );
};
And call it like this:
loading( function() {
// This take long time...
data.sortColsByLabel(!data.cols.sort.asc);
data.paint(obj);
});