javascript innerHTML de span ne change pas avant la tâche de traitement

Sep 17 2020

Chère communauté stackoverflow,

Ceci est une version très simplifiée de mon problème.

J'ai écrit un spandans mon HTML bodyet j'ai configuré le JavaScript du HTML pour changer le spantexte du HTML avant de traiter une tâche donnée.

JavaScript décide cependant de ne pas modifier le texte de la fonction spanavant de traiter la fonction FOR(). Au lieu de cela, il modifie le spantexte de l 'après avoir effectué la tâche donnée. Ignorant complètement le fait que la commande pour changer le spantexte du s est positionnée une ligne au-dessus de la commande pour effectuer la tâche de traitement.

Pourquoi et comment puis-je résoudre ce problème? (Je veux que le spantexte du 's soit modifié avant que JavaScript ne commence à traiter la tâche)

<html>
  <body>
    <button onclick="pressed()">press here</button>
    <span id="text">still the same text</span>
    <script>
        function FOR(rounds) 
        {
          var x = 0;
          for(var i=0; i<rounds; i++)
          {
              x+=Math.sqrt(i);
          }

          return(x);
        }

        function pressed() 
        {
          var text = document.getElementById("text");
          text.innerHTML = "i have changed";

          console.log(FOR(2000000000));
          console.log("processing done");
        }
    </script>
  </body>
</html>

Réponses

1 Kaiido Sep 17 2020 at 06:28

Le texte est modifié dans le DOM, de manière synchrone, mais le rendu à l'écran ne se fera que dans une itération de boucle d'événement particulière (ci-après « cadre de peinture ») où les étapes de «mise à jour du rendu» seront appelées.

Le blocage synchrone de la tâche ne permettra pas au navigateur d'atteindre cette partie de la boucle d'événements, et donc aucun rendu ne se produira entre les deux.

Vous pouvez attendre que le prochain cadre de peinture se soit déclenché avant d'appeler votre script de blocage:

function FOR(rounds) {
    var x = 0;
    for (var i = 0; i < rounds; i++) {
        x += Math.sqrt(i);
    }
    return (x);
}

function pressed() {
    var text = document.getElementById("text");
    text.innerHTML = "i have changed";
    // requestAnimationFrame schedules a callback to fire before the next rendering
    requestAnimationFrame( () => {
      // so we still need to wait the next iteration of the event loop
      setTimeout(() => {
        console.log(FOR(2000000000));
        console.log("processing done");
      },0);
    });
}
<button onclick="pressed()">press here</button>
<span id="text">still the same text</span>

Mais le mieux est de ne pas verrouiller du tout votre interface utilisateur.

Si vous avez des calculs lourds à faire, utilisez un Web-Worker, qui permettra à votre navigateur d'utiliser un autre thread pour effectuer les calculs, laissant le thread principal de l'interface utilisateur libre de faire son travail principal:

// StackSnippet can't fetch external files, so here we have to build it
// in your case you'd just have it in an external file
const worker_script  = `
function FOR(rounds) {
    var x = 0;
    for (var i = 0; i < rounds; i++) {
        x += Math.sqrt(i);
    }
    return (x);
}
self.onmessage = (evt) => {
  // post back the result
  self.postMessage( FOR( evt.data ) );
}`;
const worker_blob = new Blob([worker_script],{type:"text/javascript"});
const worker_url = URL.createObjectURL(worker_blob);

// initialise the worker
const worker = new Worker(worker_url);
const button = document.querySelector("button");
function pressed() {
  var text = document.getElementById("text");
  text.innerHTML = "i have changed " + new Date().getTime();
  // handle the results from the worker
  worker.addEventListener("message", (evt) => {
      console.log(evt.data);
      console.log("processing done");
      button.disabled = false;
  }, {once: true} );
  // start the computation
  worker.postMessage(2000000000);
  button.disabled = true;
  console.log("new computation started");
}
<button onclick="pressed()">press here</button>
<span id="text">still the same text</span>

Cependant, sachez qu'attendre un seul délai d'expiration ou pire encore une microtâche ne suffira pas. En utilisant setTimeout, vous ne pouvez pas être sûr que la trame de paiting (qui se produit normalement à la même fréquence que le taux de rafraîchissement du moniteur, c'est-à-dire toutes les 16,6 ms dans la plupart des cas généraux), se produira entre les deux. En utilisant des microtâches, vous pouvez être sûr que cela ne s'est pas produit. Un exemple prouvant que la microtâche est exécutée avant le rendu? .

2 GuerricP Sep 17 2020 at 04:40

C'est parce que le navigateur attend que la pile d'appels actuelle (les fonctions s'appelant les unes les autres) se déroule complètement avant d'appliquer toute modification à la page rendue. Pour résoudre ce problème, vous devez différer la deuxième fonction de la file d'attente de rappel avec un setTimeoutou queueMicrotask. Vous pouvez essayer de l'exécuter avec les outils de développement ouverts et vous remarquerez que la durée a changé lorsque le point d'arrêt est atteint:

<html>
    <body>
        <button onclick="pressed()">press here</button>
        <span id="text">still the same text</span>
        <script>
            function FOR(rounds) {
                var x = 0;
                for (var i = 0; i < rounds; i++) {
                    x += Math.sqrt(i);
                }
            
                return (x);
            }
            
            function pressed() {
                var text = document.getElementById("text");
                text.innerHTML = "i have changed";
            
                queueMicrotask(function () {
                    debugger;
                    console.log(FOR(2000000000));
                    console.log("processing done");
                });
            }
        </script>
    </body>
</html>