0

EDIT: While this question has been answered and very well, I wanted to share another answer I found that I think explains the issue more in depth just for the sake of learning: Javascript - Are DOM redraw methods synchronous?

Rock Paper Scissors Github Project

The issue: I am writing a program Rock Paper Scissors for The Odin Project. This calls a function updateElements() to change the textContent of on page variables to display the round statistics. This functions works properly until the win condition is met in the function roundCounter(); however, roundCounter() should not be called before the function updateElements() completes.

updateElements() -> roundCounter() -> globalReset()

A win or loss condition is met when any of the round counting variables reaches 3. This can result in a win, draw, or loss. In any condition being met a prompt occurs which asks the player to either begin a new game or not. This is my error, it seems that the win condition is somehow met prior to updateElements() being able to finish updating the DOM.

Console logs left from when it was console only, show that the variables are on the correct iteration. One possible solution that my mentor and I worked on was causing a timeOut to occur prior to roundCounter() being called. This does work if set to anything greater than 20ms. Yes this is a solution. I am here asking for more understanding on what and why this issue is happening. Why does the DOM not draw the new values before the next function? Why does the prompt stop the DOM from updating? Any help would be much appreciated!

function updateElements() {
  let pScore = `${playerWins}`;
  let cScore = `${computerWins}`;
  let dCount = `${nobodyWins}`;
  let rCount = `${roundCount}`;
  PLAYER_SCORE.textContent = pScore
  COMPUTER_SCORE.textContent = cScore;
  DRAW_COUNT.textContent = dCount;
  ROUND_COUNT.textContent = rCount;
  roundCounter();
}

Which then moves to roundCounter()

function roundCounter() {
  console.log(`Your wins: ${playerWins} \nComputer Wins: ${computerWins} \nDraws: ${nobodyWins} \nRound Count: ${roundCount}`);
  if (roundCount === 5 && computerWins < 3 && playerWins < 3 && nobodyWins < 3 ) {
    console.log("This game was a draw");
    newGamePrompt();
  } else if (roundCount <= 5) {
    if (playerWins === 3) {
      console.log("You are the winner!");
      newGamePrompt();
    } else if (computerWins === 3) {
      console.log("You lose!");
      newGamePrompt();
    } else if (nobodyWins === 3) {
      console.log("Nobody won!");
      newGamePrompt();
    }
  } else if (roundCount > 5) {
    console.log(
      "An error has occured: The game has run too many rounds. Restarting");
    newGameYes();
  }
}

Prompt displays before DOM updates

Canceling the Prompt causes DOM to finish updating

Troubleshooting Steps taken:

Removing the newGamePrompt(), setting playAgain locally to "no"

  • no change.

Debugger:

  • Counter updates appropriately.
  • Executes in appropriate order.

console logging:

  • updateElements() and roundCounter() show the correct value.

1 Answers1

0

There are 2 things to know here:

  • When you use prompt (or its siblings alert and confirm), everything on the page stops until the box is dismissed. (other JS doesn't run, and the browser can't repaint.)

  • The browser waits to repaint the screen until the main thread's JavaScript execution is finished.

If you change the DOM, then immediately call prompt, the DOM update will not be seen until after the prompt finishes and the thread ends.

Here's a simpler example of this problem in action:

document.body.innerHTML += 'you will not see this until dismissing the prompt';
prompt('foo');

Doing something like putting further logic inside a small setTimeout is a very common solution to this problem (but more elegant would be requestPostAnimationFrame once it gets supported, or with its polyfill, thanks Kaiido).

An even better solution would be to avoid prompt and its siblings entirely, since they block the page - they're so user-unfriendly and can cause unintuitive behavior like this. Create a proper modal instead. It'll take a bit more code, but it's a much more elegant solution.

CertainPerformance
  • 356,069
  • 52
  • 309
  • 320
  • To people using Firefox and seeing the text rendered contrarily to what this answer states, as [I explained here](https://stackoverflow.com/a/52326008/3702797), there is actually a note in the specs asking implementers to not follow the specs in this case, so that they spin-the-event-loop and don't just block the UI completely. And the official replacement for modals is the [](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/dialog) element. (And a last nit, [the DOM is "updated" synchronously](https://stackoverflow.com/a/47343090/3702797), but it is repainted asynchronously) – Kaiido Mar 11 '21 at 01:41
  • Excellent, thank you! So my next thought was to implement just physical buttons that appeared when the round conditions were met. Is there a better solution to this? It's rather open ended how I go about this, I am trying to learn best practices here and see what other people would do/avoid. – Michael Braley Mar 11 '21 at 01:51
  • @MichaelBraley You mean ` – CertainPerformance Mar 11 '21 at 01:53
  • Exactly it. Thank you. So I am curious. Why does setTimeout allow this to work if it's the same thread? Is the DOM just catching up? – Michael Braley Mar 11 '21 at 01:57
  • @MichaelBraley The DOM only gets repainted (in Chrome, at least) after all synchronous JS has finished executing. In the code in the snippet, that occurs only after the `prompt` call ends. In your script, it only occurs after the `roundCounter` function ends (which is after the `prompt`). A `setTimeout` queues the callback to after the sync JS has finished, giving the browser time to repaint. – CertainPerformance Mar 11 '21 at 01:59
  • 1
    If I may once more, regarding `setTimeout` it's a dice roll. The repaint doesn't happen necessarily right after the synchronous task, like microtasks do for instance. Instead, it happens at the next *painting frame*, which is linked to the monitor's refresh rate. So you may very well have your `setTimeout` callback fire before that *painting frame*. To be 100% sure, the best is to call `requestPostAnimationFrame(cb)` in Chrome, or `requestAnimationFrame(()=>setTimeout(cb,0))` in others. – Kaiido Mar 11 '21 at 02:10