Wait until setInterval() is done

2019-04-05 04:27发布

问题:

I would like to add a small dice rolling effect to my Javascript code. I think a good ways is to use the setInterval() method. My idea was following code (just for testing):

function roleDice() {
        var i = Math.floor((Math.random() * 25) + 5);   
        var j = i;
        var test = setInterval(function(){
            i--;
            document.getElementById("dice").src = "./images/dice/dice" + Math.floor((Math.random() * 6) + 1) + ".png";
            if(i < 1) {
                    clearInterval(test);
                }

            }, 50);     
    }

Now I would like to wait for the setInterval until it is done. So I added a setTimeout.

setTimeout(function(){alert("test")}, (j + 1) * 50);

This code works quite okay. But in my main code the roleDice() function returns a value. Now I don’t know how I could handle that... I can’t return from the setTimeout(). If I add a return to the end of the function, the return will raised to fast. Does anyone have a idea, how I could fix that?

Edit Hmm, okay I understand what the callback dose and I think I know how it works but I have still the problem. I think it’s more a "interface" problem... Here my code:

function startAnimation(playername, callback) {
    var i = Math.floor((Math.random() * 25) + 5);
    var int = setInterval(function() {
        i--;
        var number = Math.floor((Math.random() * 6) + 1);
        document.getElementById("dice").src = "./images/dice/dice" + number + ".png";
        if(i < 1) {
            clearInterval(int);
            number = Math.floor((Math.random() * 6) + 1);
            addText(playername + " rolled " + number);
            document.getElementById("dice").src = "./images/dice/dice" + number + ".png";
            callback(number);
        }
    }, 50);
}

function rnd(playername) {
    var callback = function(value){
        return value; // I knew thats pointless...
    };
    startAnimation(playername, callback);
}

The function rnd() should wait and return the value… I’m a little bit confused. At the moment I have no clue how to going on... The code wait for the var callback... but how I could combined it with the return? I would like to run the animation and return after that the last number with rnd() to a other function.

回答1:

You stumbled into the pitfall most people hit at some point when they get in touch with asynchronous programming.

You cannot "wait" for an timeout/interval to finish - trying to do so would not work or block the whole page/browser. Any code that should run after the delay needs to be called from the callback you passed to setInterval when it's "done".

function rollDice(callback) {
    var i = Math.floor((Math.random() * 25) + 5);
    var j = i;
    var test = setInterval(function() {
        i--;
        var value = Math.floor((Math.random() * 6) + 1);
        document.getElementById("dice").src = "./images/dice/dice" + value + ".png";
        if(i < 1) {
            clearInterval(test);
            callback(value);
        }
    }, 50);
}

You then use it like this:

rollDice(function(value) {
    // code that should run when the dice has been rolled
});


回答2:

UPDATE on TheifMaster's Answer:

You can now use Promises

Like callbacks you can use Promises to pass a function that is called when the program is done running, if you use reject you can also handle errors with Promises.

function rollDice() {
  return new Promise((resolve, reject) => {
    const dice = document.getElementById("dice")

    let i = Math.floor((Math.random() * 25) + 5)

    const intervalId = setInterval(() => {
      const diceValue = Math.floor((Math.random() * 6) + 1)

      dice.src = `./images/dice/dice${diceValue}.png`

      if (--i < 1) {
        clearInterval(intervalId)
        resolve(diceValue)
      }
    }, 50)
  })
}

Then use it like this:

rollDice().then(value => alert(`Dice rolled: ${value}`))


回答3:

Orginally your code was all sequential. Here is a basic dice game where two players roll one and they see who has a bigger number. [If a tie, second person wins!]

function roleDice() {
    return Math.floor(Math.random() * 6) + 1;
}

function game(){    
    var player1 = roleDice(),
        player2 = roleDice(),
        p1Win = player1 > player2;
    alert( "Player " + (p1Win ? "1":"2") + " wins!" );
}

game();

The code above is really simple since it just flows. When you put in a asynchronous method like that rolling the die, you need to break up things into chunks to do processing.

function roleDice(callback) {
    var i = Math.floor((Math.random() * 25) + 5);   
    var j = i;
    var test = setInterval(function(){
        i--;
        var die =  Math.floor((Math.random() * 6) + 1);
        document.getElementById("dice").src = "./images/dice/dice" + die + ".png";
        if(i < 1) {
                clearInterval(test);
                callback(die);  //Return the die value back to a function to process it
            }
        }, 50);
}

function game(){
    var gameInfo = {  //defaults
                       "p1" : null,
                       "p2" : null
                   },
        playerRolls = function (playerNumber) { //Start off the rolling
            var callbackFnc = function(value){ //Create a callback that will 
                playerFinishes(playerNumber, value); 
            };
            roleDice( callbackFnc );
        },
        playerFinishes = function (playerNumber, value) { //called via the callback that role dice fires
            gameInfo["p" + playerNumber] = value;
            if (gameInfo.p1 !== null && gameInfo.p2 !== null ) { //checks to see if both rolls were completed, if so finish game
                giveResult();
            }
        },
        giveResult = function(){ //called when both rolls are done
            var p1Win = gameInfo.p1 > gameInfo.p2;
            alert( "Player " + (p1Win ? "1":"2") + " wins!" );
        };            
    playerRolls("1");  //start player 1
    playerRolls("2");  //start player 2
}

game();

The above code could be better in more of an OOP type of way, but it works.



回答4:

There are a few issues for the above solutions to work. Running the program doesn't (at least not in my preferred browser) show any images, so these has to be loaded before running the game.

Also, by experience I find the best way to initiate the callback method in cases like preloading N images or having N players throw a dice is to let each timeout function do a countdown to zero and at that point execute the callback. This works like a charm and does not rely on how many items needing to be processed.

<html><head><script>
var game = function(images){
   var nbPlayers = 2, winnerValue = -1, winnerPlayer = -1;
   var rollDice = function(player,callbackFinish){
      var playerDice = document.getElementById("dice"+player);
      var facesToShow = Math.floor((Math.random() * 25) + 5);   
      var intervalID = setInterval(function(){
         var face =  Math.floor(Math.random() * 6);
         playerDice.src = images[face].src;
         if (--facesToShow<=0) {
            clearInterval(intervalID);
            if (face>winnerValue){winnerValue=face;winnerPlayer=player}
            if (--nbPlayers<=0) finish();
         }
      }, 50);
   }
   var finish = function(){
      alert("Player "+winnerPlayer+" wins!");
   }      
   setTimeout(function(){rollDice(0)},10);
   setTimeout(function(){rollDice(1)},10);
}
var preloadImages = function(images,callback){
   var preloads = [], imagesToLoad = images.length;
   for (var i=0;i<images.length;++i){
      var img=new Image();
      preloads.push(img);
      img.onload=function(){if(--imagesToLoad<=0)callback(preloads)}
      img.src = images[i];
   }
}
preloadImages(["dice1.png","dice2.png","dice3.png","dice4.png","dice5.png","dice6.png"],game);
</script></head><body>
<img src="" id="dice0" /><img src="" id="dice1" /></body></html>