javascript innerHTML of span não muda antes de processar a tarefa

Sep 17 2020

Cara comunidade stackoverflow,

Esta é uma versão muito simplificada do meu problema.

Eu escrevi um spanno meu HTML bodye configurei o JavaScript do HTML para alterar o spantexto do antes de processar uma determinada tarefa.

JavaScript, entretanto, decide não alterar o texto de spanantes de processar a função FOR(). Em vez disso, ele altera o spantexto de após concluir a tarefa fornecida. Ignorando completamente o fato de que o comando para alterar o spantexto de está posicionado uma linha acima do comando para realizar a tarefa de processamento.

Por que isso acontece e como posso corrigir isso? (Quero que o spantexto de seja alterado antes que o JavaScript comece a processar a tarefa)

<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>

Respostas

1 Kaiido Sep 17 2020 at 06:28

O texto é alterado no DOM, de forma síncrona, mas a renderização para a tela será feita apenas em uma iteração de loop de evento específica (doravante " quadro de pintura ") onde as etapas de "atualizar a renderização" serão chamadas.

O bloqueio síncrono da tarefa não permitirá que o navegador alcance esta parte do loop de eventos e, portanto, nenhuma renderização ocorrerá no meio.

Você pode esperar que o próximo quadro de pintura seja disparado antes de chamar seu script de bloqueio:

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>

Mas o melhor é não bloquear sua IU.

Se você tiver que fazer cálculos pesados, use um Web-Worker, que permitirá que seu navegador use outro thread para realizar os cálculos, deixando o thread de IU principal livre para fazer seu trabalho 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>

No entanto, tome cuidado, pois esperar apenas um único tempo limite ou, pior ainda, por microtarefa, não será suficiente. Usando setTimeout, você não pode ter certeza de que o quadro de pausa (que normalmente ocorre na mesma frequência que a taxa de atualização do monitor, ou seja, a cada 16,6 ms na maioria dos casos gerais), ocorrerá no meio. Usando microtarefas, você pode ter certeza que não terá ocorrido. Algum exemplo provando que a microtarefa foi executada antes da renderização? .

2 GuerricP Sep 17 2020 at 04:40

Isso porque o navegador espera que a pilha de chamadas atual (funções que chamam umas às outras) se desenrole completamente antes de aplicar qualquer alteração à página renderizada. Para resolver este problema, você deve adiar a segunda função na fila de retorno de chamada com um setTimeoutou queueMicrotask. Você pode tentar executar isso com as ferramentas de desenvolvimento abertas e você notará que o intervalo mudou quando o ponto de interrupção foi atingido:

<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>