Por que setTimeout (fn, 0) às vezes é útil?

872

Recentemente, encontrei um bug bastante desagradável, em que o código estava carregando <select>dinamicamente via JavaScript. Este carregado dinamicamente <select>tinha um valor pré-selecionado. No IE6, já tínhamos código para corrigir o selecionado <option>, porque às vezes o <select>'s selectedIndexvalor estaria fora de sincronia com o selecionado <option>de indexatributo, como abaixo:

field.selectedIndex = element.index;

No entanto, esse código não estava funcionando. Mesmo que o campo selectedIndexestivesse sendo definido corretamente, o índice errado acabaria sendo selecionado. No entanto, se eu colocasse uma alert()declaração no momento certo, a opção correta seria selecionada. Pensando que isso pode ser algum tipo de problema de tempo, tentei algo aleatório que já havia visto no código:

var wrapFn = (function() {
    var myField = field;
    var myElement = element;

    return function() {
        myField.selectedIndex = myElement.index;
    }
})();
setTimeout(wrapFn, 0);

E isso funcionou!

Eu tenho uma solução para o meu problema, mas não me preocupo por não saber exatamente por que isso resolve o meu problema. Alguém tem uma explicação oficial? Que problema no navegador estou evitando ao chamar minha função "mais tarde" usando setTimeout()?

Dan Lew
fonte
2
A maioria das perguntas descreve por que é útil. Se você precisa saber por que isso acontece - leia minha resposta: stackoverflow.com/a/23747597/1090562
Salvador Dali
18
Philip Roberts explica isso da melhor maneira possível aqui em sua palestra "O que diabos é o loop de eventos?" youtube.com/watch?v=8aGhZQkoFbQ
vasa
Se você estiver com pressa, essa é a parte do vídeo que ele começa a responder exatamente à pergunta: youtu.be/8aGhZQkoFbQ?t=14m54s . Independentemente disso, o vídeo inteiro vale a pena assistir, com certeza.
William Hampshire
4
setTimeout(fn)é o mesmo que setTimeout(fn, 0), btw.
Vsync

Respostas:

827

Na pergunta, existia uma condição de corrida entre:

  1. A tentativa do navegador de inicializar a lista suspensa, pronta para atualizar o índice selecionado, e
  2. Seu código para definir o índice selecionado

Seu código estava vencendo consistentemente esta corrida e tentando definir a seleção suspensa antes que o navegador estivesse pronto, o que significa que o bug seria exibido.

Essa corrida existiu porque o JavaScript tem um único encadeamento de execução que é compartilhado com a renderização da página. Com efeito, a execução do JavaScript bloqueia a atualização do DOM.

Sua solução alternativa foi:

setTimeout(callback, 0)

Chamar setTimeoutcom um retorno de chamada e zero como o segundo argumento agendará o retorno de chamada para ser executado de forma assíncrona , após o menor atraso possível - que será de cerca de 10 ms quando a guia estiver focada e o encadeamento de execução JavaScript não estiver ocupado.

A solução do OP, portanto, foi adiar em cerca de 10 ms, a configuração do índice selecionado. Isso deu ao navegador a oportunidade de inicializar o DOM, corrigindo o erro.

Todas as versões do Internet Explorer exibiam comportamentos peculiares e esse tipo de solução alternativa era necessária às vezes. Como alternativa, pode ter sido um bug genuíno na base de código do OP.


Veja Philip Roberts falar "O que diabos é o loop de eventos?" para uma explicação mais completa.

staticsan
fonte
276
'A solução é "pausar" a execução do JavaScript para permitir que os threads de renderização sejam atualizados.' Não é inteiramente verdade, o que setTimeout faz é adicionar um novo evento à fila de eventos do navegador e o mecanismo de renderização já está nessa fila (não é totalmente verdade, mas é suficientemente próximo) para que seja executado antes do evento setTimeout.
David Mulder
48
Sim, essa é uma resposta muito mais detalhada e muito mais correta. Mas o meu é "correto o suficiente" para as pessoas entenderem por que o truque funciona.
staticsan
2
@DavidMulder, isso significa que o navegador analisa css e renderiza em um thread diferente do thread de execução do javascript?
jason
8
Não, eles são analisados ​​em princípio no mesmo encadeamento, caso contrário, algumas linhas de manipulação do DOM acionariam refluxos o tempo todo, o que teria uma influência extremamente ruim na velocidade de execução.
David Mulder
30
Este vídeo é a melhor explicação para o motivo de definirmos o timeout
davibq
665

Prefácio:

Algumas das outras respostas estão corretas, mas na verdade não ilustram qual é o problema que está sendo resolvido, então criei essa resposta para apresentar essa ilustração detalhada.

Como tal, estou postando uma explicação detalhada sobre o que o navegador faz e como o uso setTimeout()ajuda . Parece longo, mas na verdade é muito simples e direto - eu apenas o deixei bem detalhado.

ATUALIZAÇÃO: Eu criei um JSFiddle para demonstrar ao vivo a explicação abaixo: http://jsfiddle.net/C2YBE/31/ . Muito obrigado a @ThangChung por ajudar a dar o pontapé inicial.

UPDATE2: Caso o site JSFiddle morra ou exclua o código, eu adicionei o código a esta resposta no final.


DETALHES :

Imagine um aplicativo Web com um botão "faça alguma coisa" e uma div de resultado.

O onClickmanipulador do botão "fazer alguma coisa" chama uma função "LongCalc ()", que faz duas coisas:

  1. Faz um cálculo muito longo (digamos, leva 3 min)

  2. Imprime os resultados do cálculo na div resultado.

Agora, seus usuários começam a testar isso, clique no botão "fazer alguma coisa" e a página fica lá aparentemente sem fazer nada por 3 minutos, ficam inquietos, clique no botão novamente, aguarde 1 minuto, nada acontece, clique no botão novamente ...

O problema é óbvio - você quer um DIV "Status", que mostra o que está acontecendo. Vamos ver como isso funciona.


Então você adiciona um DIV "Status" (inicialmente vazio) e modifica o onclickmanipulador (função LongCalc()) para fazer 4 coisas:

  1. Preencha o status "Calcular ... pode levar ~ 3 minutos" para o status DIV

  2. Faz um cálculo muito longo (digamos, leva 3 min)

  3. Imprime os resultados do cálculo na div resultado.

  4. Preencher o status "Cálculo realizado" no status DIV

E, felizmente, você fornece o aplicativo aos usuários para testar novamente.

Eles voltam para você com muita raiva. E explique que, quando clicaram no botão, o Status DIV nunca foi atualizado com o status "Calculating ..." !!!


Você coça a cabeça, pergunta no StackOverflow (ou lê documentos ou google) e percebe o problema:

O navegador coloca todas as suas tarefas "TODO" (tarefas da interface do usuário e comandos JavaScript) resultantes de eventos em uma única fila . E, infelizmente, redesenhar o DIV "Status" com o novo valor "Calculating ..." é um TODO separado que vai para o final da fila!

Aqui está um detalhamento dos eventos durante o teste do usuário, o conteúdo da fila após cada evento:

  • Fila: [Empty]
  • Evento: Clique no botão Fila após evento:[Execute OnClick handler(lines 1-4)]
  • Evento: Execute a primeira linha no manipulador OnClick (por exemplo, altere o valor do Status DIV). Fila após o evento: [Execute OnClick handler(lines 2-4), re-draw Status DIV with new "Calculating" value]. Observe que, enquanto as alterações no DOM acontecem instantaneamente, para redesenhar o elemento DOM correspondente, você precisa de um novo evento, acionado pela alteração no DOM, que foi no final da fila .
  • PROBLEMA!!! PROBLEMA!!! Detalhes explicados abaixo.
  • Evento: Execute a segunda linha no manipulador (cálculo). Fila após: [Execute OnClick handler(lines 3-4), re-draw Status DIV with "Calculating" value].
  • Evento: Execute a terceira linha no manipulador (preencha o resultado DIV). Fila após: [Execute OnClick handler(line 4), re-draw Status DIV with "Calculating" value, re-draw result DIV with result].
  • Evento: Execute a quarta linha no manipulador (preencha o status DIV com "DONE"). Fila: [Execute OnClick handler, re-draw Status DIV with "Calculating" value, re-draw result DIV with result; re-draw Status DIV with "DONE" value].
  • Evento: execução implícita returndo onclickmanipulador sub. Retiramos o manipulador "Execute OnClick" da fila e começamos a executar o próximo item na fila.
  • NOTA: Como já concluímos o cálculo, já passaram 3 minutos para o usuário. O evento re-draw ainda não aconteceu !!!
  • Evento: redesenha o DIV de status com o valor "Calculating". Fazemos o redesenho e tiramos isso da fila.
  • Evento: redesenhar o resultado DIV com o valor do resultado. Fazemos o redesenho e tiramos isso da fila.
  • Evento: redesenha o DIV de status com o valor "Concluído". Fazemos o redesenho e tiramos isso da fila. Os espectadores com olhos atentos podem até notar o "Status DIV com o valor" Calculating "piscando por uma fração de microssegundo - APÓS O CÁLCULO TERMINADO

Portanto, o problema subjacente é que o evento de redesenho para a DIV "Status" é colocado na fila no final, APÓS o evento "linha de execução 2", que leva 3 minutos, para que o redesenho real não ocorra até APÓS o cálculo é feito.


Para o resgate vem o setTimeout(). Como isso ajuda? Porque, ao chamar o código de execução longa setTimeout, você cria 2 eventos: a setTimeoutprópria execução e (devido ao tempo limite de 0), separa a entrada da fila para o código que está sendo executado.

Portanto, para corrigir seu problema, você modifica seu onClickmanipulador para DUAS instruções (em uma nova função ou apenas em um bloco onClick):

  1. Preencha o status "Calcular ... pode levar ~ 3 minutos" para o status DIV

  2. Execute setTimeout()com tempo limite 0 e uma chamada para LongCalc()função .

    LongCalc()a função é quase a mesma da última vez, mas obviamente não possui a atualização DIV do status "Calculating ..." como primeira etapa; e inicia o cálculo imediatamente.

Então, como é a sequência de eventos e a fila agora?

  • Fila: [Empty]
  • Evento: Clique no botão Fila após evento:[Execute OnClick handler(status update, setTimeout() call)]
  • Evento: Execute a primeira linha no manipulador OnClick (por exemplo, altere o valor do Status DIV). Fila após o evento: [Execute OnClick handler(which is a setTimeout call), re-draw Status DIV with new "Calculating" value].
  • Evento: Execute a segunda linha no manipulador (chamada setTimeout). Fila após: [re-draw Status DIV with "Calculating" value]. A fila não tem novidades por mais 0 segundos.
  • Evento: o alarme do tempo limite dispara, 0 segundos depois. Fila após: [re-draw Status DIV with "Calculating" value, execute LongCalc (lines 1-3)].
  • Evento: redesenha o DIV de status com o valor "Calculating" . Fila após: [execute LongCalc (lines 1-3)]. Observe que esse evento de redesenho pode realmente acontecer ANTES do alarme tocar, o que também funciona.
  • ...

Viva! O Status DIV acabou de ser atualizado para "Calculando ..." antes do início do cálculo !!!



Abaixo está o código de amostra do JSFiddle que ilustra estes exemplos: http://jsfiddle.net/C2YBE/31/ :

Código HTML:

<table border=1>
    <tr><td><button id='do'>Do long calc - bad status!</button></td>
        <td><div id='status'>Not Calculating yet.</div></td>
    </tr>
    <tr><td><button id='do_ok'>Do long calc - good status!</button></td>
        <td><div id='status_ok'>Not Calculating yet.</div></td>
    </tr>
</table>

Código JavaScript: (executado em onDomReadye pode exigir o jQuery 1.9)

function long_running(status_div) {

    var result = 0;
    // Use 1000/700/300 limits in Chrome, 
    //    300/100/100 in IE8, 
    //    1000/500/200 in FireFox
    // I have no idea why identical runtimes fail on diff browsers.
    for (var i = 0; i < 1000; i++) {
        for (var j = 0; j < 700; j++) {
            for (var k = 0; k < 300; k++) {
                result = result + i + j + k;
            }
        }
    }
    $(status_div).text('calculation done');
}

// Assign events to buttons
$('#do').on('click', function () {
    $('#status').text('calculating....');
    long_running('#status');
});

$('#do_ok').on('click', function () {
    $('#status_ok').text('calculating....');
    // This works on IE8. Works in Chrome
    // Does NOT work in FireFox 25 with timeout =0 or =1
    // DOES work in FF if you change timeout from 0 to 500
    window.setTimeout(function (){ long_running('#status_ok') }, 0);
});
DVK
fonte
12
ótima resposta DVK! Aqui é uma essência que ilustra o exemplo gist.github.com/kumikoda/5552511#file-timeout-html
kumikoda
4
Resposta muito legal, DVK. Apenas para tornar fácil imaginar que ponho esse código para jsFiddle jsfiddle.net/thangchung/LVAaV
thangchung
4
@ThangChung - Tentei criar uma versão melhor (2 botões, um para cada caso) no JSFiddle. Funciona como uma demonstração no Chrome e IE, mas não no FF por algum motivo - consulte jsfiddle.net/C2YBE/31 . Perguntei por que FF não funciona aqui: stackoverflow.com/questions/20747591/...
DVK
1
@DVK "O navegador coloca todas as tarefas" TODO "(tarefas da interface do usuário e comandos JavaScript) resultantes de eventos em uma única fila". Senhor, você pode fornecer a fonte para isso? Imho Arent navegadores suposto ter UI diferente (motor de renderização) e tópicos JS .... sem querer ofender .... só quero saber ..
bhavya_w
1
@bhavya_w Não, tudo acontece em um tópico. Esta é a razão um cálculo longos js pode bloquear a interface do usuário
Seba Kerckhof
23

setTimeout() leva algum tempo até que os elementos DOM sejam carregados, mesmo se for definido como 0.

Confira: setTimeout

Jose Basilio
fonte
23

A maioria dos navegadores possui um processo chamado thread principal, responsável por executar algumas tarefas JavaScript, atualizações da interface do usuário, por exemplo: pintura, redesenho ou refluxo, etc.

Algumas tarefas de execução do JavaScript e de atualização da interface do usuário são colocadas em fila na fila de mensagens do navegador e, em seguida, são despachadas para o encadeamento principal do navegador a ser executado.

Quando as atualizações da interface do usuário são geradas enquanto o encadeamento principal está ocupado, as tarefas são adicionadas à fila de mensagens.

setTimeout(fn, 0);adicione isso fnao final da fila a ser executada. Ele agenda uma tarefa a ser adicionada na fila de mensagens após um determinado período de tempo.

arley
fonte
"Todas as tarefas de execução de JavaScript e atualização de interface do usuário são adicionadas ao sistema de fila de eventos do navegador, e essas tarefas são despachadas para o Thread da interface do usuário principal do navegador a ser executado." .... fonte, por favor?
bhavya_w
4
JavaScript de alto desempenho (Nicholas Zakas, Stoyan Stefanov, Ross Harmes, Julien Lecomte e Matt Sweeney)
Arley
Voto negativo para isso add this fn to the end of the queue. O mais importante é onde exatamente setTimeoutadiciona essa função, o final deste ciclo de loop ou o início do próximo ciclo de loop.
Verde
19

Aqui existem respostas conflitantes aprovadas e sem provas não há como saber em quem acreditar. Aqui está a prova de que o @DVK está certo e o @SalvadorDali está incorreto. O último afirma:

"E aqui está o porquê: não é possível definir setTimeout com um atraso de 0 milissegundos. O valor Mínimo é determinado pelo navegador e não é 0 milissegundos. Historicamente, os navegadores definem esse mínimo como 10 milissegundos, mas as especificações e os HTML5 navegadores modernos têm 4 milissegundos ".

O tempo limite mínimo de 4 ms é irrelevante para o que está acontecendo. O que realmente acontece é que setTimeout envia a função de retorno de chamada para o final da fila de execução. Se após setTimeout (retorno de chamada, 0) você tiver um código de bloqueio que leva vários segundos para ser executado, o retorno de chamada não será executado por alguns segundos, até que o código de bloqueio seja concluído. Tente este código:

function testSettimeout0 () {
    var startTime = new Date().getTime()
    console.log('setting timeout 0 callback at ' +sinceStart())
    setTimeout(function(){
        console.log('in timeout callback at ' +sinceStart())
    }, 0)
    console.log('starting blocking loop at ' +sinceStart())
    while (sinceStart() < 3000) {
        continue
    }
    console.log('blocking loop ended at ' +sinceStart())
    return // functions below
    function sinceStart () {
        return new Date().getTime() - startTime
    } // sinceStart
} // testSettimeout0

A saída é:

setting timeout 0 callback at 0
starting blocking loop at 5
blocking loop ended at 3000
in timeout callback at 3033
Vladimir Kornea
fonte
sua resposta não prova nada. Isso apenas mostra que em sua máquina, em uma situação específica, o computador gera alguns números. Para provar algo relacionado, você precisa de um pouco mais do que algumas linhas de código e alguns números.
Salvador Dali
6
@SalvadorDali, acredito que minha prova é clara o suficiente para a maioria das pessoas entender. Acho que você está se sentindo defensivo e não fez um esforço para entendê-lo. Ficarei feliz em tentar esclarecer isso, mas não sei o que você está deixando de entender. Tente executar o código em sua própria máquina se suspeitar dos meus resultados.
Vladimir Kornea
Vou tentar deixar claro pela última vez. Minha resposta é esclarecer algumas das propriedades interessantes em timeout, interval e é bem suportada por fontes válidas e reconhecíveis. Você fez uma forte afirmação de que está errado e apontou para seu pedaço de código que, por algum motivo, nomeou uma prova. Não há nada errado com o seu pedaço de código (não sou contra). Estou apenas dizendo que minha resposta não está errada. Esclarece alguns pontos. Vou parar esta guerra, porque não vejo razão.
Salvador Dali
15

Uma razão para fazer isso é adiar a execução do código para um loop de eventos subsequente separado. Ao responder a algum tipo de evento do navegador (clique do mouse, por exemplo), às vezes é necessário executar operações somente após o processamento do evento atual. A setTimeout()instalação é a maneira mais simples de fazê-lo.

editar agora que é 2015, devo observar que também há requestAnimationFrame(), o que não é exatamente o mesmo, mas é suficientemente próximo do setTimeout(fn, 0)que vale a pena mencionar.

Pontudo
fonte
Este é precisamente um dos lugares onde eu o vi sendo usado. =)
Zaibot
FYI: esta resposta foi mesclada aqui de stackoverflow.com/questions/4574940/…
Shog9
2
é adiar a execução do código para um loop de eventos subsequente separado : como você descobre um loop de eventos subsequente ? Como você descobre o que é um loop de evento atual? Como você sabe em que curva de evento você está agora?
Verde
@ Verde bem, você realmente não; realmente não há visibilidade direta sobre o que o tempo de execução do JavaScript está planejando.
Pointy
requestAnimationFrame resolvido o problema que eu estava tendo com IE e Firefox não atualizar UI, por vezes
David
9

Esta é uma perguntas antigas com respostas antigas. Eu queria adicionar um novo olhar para esse problema e responder por que isso acontece e não por que isso é útil.

Então você tem duas funções:

var f1 = function () {    
   setTimeout(function(){
      console.log("f1", "First function call...");
   }, 0);
};

var f2 = function () {
    console.log("f2", "Second call...");
};

e depois chame-os na seguinte ordem f1(); f2(); apenas para ver se o segundo foi executado primeiro.

E aqui está o porquê: não é possível ter setTimeoutum atraso de 0 milissegundos. O valor mínimo é determinado pelo navegador e não é 0 milissegundos. Historicamente, os navegadores definem esse mínimo para 10 milissegundos, mas as especificações do HTML5 e os navegadores modernos o definem para 4 milissegundos.

Se o nível de aninhamento for maior que 5 e o tempo limite for menor que 4, aumente o tempo limite para 4.

Também do mozilla:

Para implementar um tempo limite de 0 ms em um navegador moderno, você pode usar window.postMessage () conforme descrito aqui .

As informações do PS são obtidas após a leitura do artigo a seguir .

Salvador Dalí
fonte
1
@ user2407309 Você está brincando? quer dizer que a especificação HTML5 está errada e você está correto? Leia as fontes antes de votar e faça fortes reivindicações. Minha resposta é baseada na especificação HTML e no registro histórico. Em vez de fazer a resposta que explica completamente as mesmas coisas várias vezes, adicionei algo novo, algo que não foi mostrado nas respostas anteriores. Não estou dizendo que esse é o único motivo, estou apenas mostrando algo novo.
Salvador Dali
1
Isso está incorreto: "E aqui está o porquê: não é possível ter setTimeout com um atraso de 0 milissegundos." Não é por isso que. O atraso de 4 ms é irrelevante para o motivo de setTimeout(fn,0)ser útil.
Vladimir Kornea
@ user2407309 pode ser facilmente modificado para "para adicionar aos motivos declarados por outros, não é possível ....". Portanto, é ridículo diminuir o voto apenas por causa disso, especialmente se a sua própria resposta não está dizendo nada de novo. Apenas uma pequena edição seria suficiente.
Salvador Dali
10
Salvador Dali: Se você ignorar os aspectos emocionais da guerra das micro chamas aqui, provavelmente precisará admitir que @VladimirKornea está certo. É verdade que os navegadores mapeiam um atraso de 0ms para 4ms, mas mesmo se não o fizerem, os resultados ainda serão os mesmos. O mecanismo de acionamento aqui é que o código é enviado para a fila, e não para a pilha de chamadas. Ter um olhar para este excelente apresentação JSConf, ele pode ajudar a esclarecer a questão: youtube.com/watch?v=8aGhZQkoFbQ
hashchange
1
Estou confuso sobre o motivo pelo qual você acha que sua cotação em um mínimo qualificado de 4 ms é um mínimo global de 4 ms. Como mostra sua citação da especificação HTML5, o mínimo é de 4 ms somente quando você aninha chamadas para setTimeout/ setIntervalmais de cinco níveis; se você não tiver, o mínimo é 0 ms (com falta de máquinas do tempo). Os documentos da Mozilla expandem isso para cobrir casos repetidos, não apenas aninhados (portanto, setIntervalcom o intervalo de 0 será reagendado imediatamente algumas vezes e depois será adiado por mais tempo), mas os usos simples de setTimeoutcom aninhamento mínimo são permitidos na fila imediatamente.
ShadowRanger
8

Como está sendo passado uma duração de 0, suponho que seja para remover o código passado para setTimeouto fluxo de execução. Portanto, se for uma função que pode demorar um pouco, não impedirá a execução do código subsequente.

user113716
fonte
FYI: esta resposta foi mesclada aqui de stackoverflow.com/questions/4574940/…
Shog9
7

Ambas as duas respostas mais bem avaliadas estão erradas. Confira a descrição do MDN no modelo de simultaneidade e no loop de eventos , e deve ficar claro o que está acontecendo (esse recurso MDN é uma verdadeira jóia). E simplesmente usar setTimeout pode adicionar problemas inesperados ao seu código, além de "resolver" esse pequeno problema.

O que realmente está acontecendo aqui não é que "o navegador ainda não esteja totalmente pronto porque é simultâneo" ou algo baseado em "cada linha é um evento que é adicionado à parte de trás da fila".

O jsfiddle fornecido pelo DVK ilustra de fato um problema, mas sua explicação para ele não está correta.

O que está acontecendo em seu código é que ele primeiro anexa um manipulador de clickeventos ao evento no #dobotão.

Em seguida, quando você realmente clica no botão, messageé criado um referenciando a função de manipulador de eventos, que é adicionada ao message queue. Quando event loopchega a essa mensagem, ele cria um framena pilha, com a chamada de função para o manipulador de eventos click no jsfiddle.

E é aqui que fica interessante. Estamos tão acostumados a pensar no Javascript como assíncrono que estamos propensos a ignorar esse pequeno fato: qualquer quadro deve ser executado, na íntegra, antes que o próximo quadro possa ser executado . Sem concorrência, pessoal.

O que isto significa? Isso significa que sempre que uma função é chamada da fila de mensagens, ela bloqueia a fila até que a pilha gerada seja esvaziada. Ou, em termos mais gerais, ele bloqueia até que a função retorne. E bloqueia tudo , incluindo operações de renderização DOM, rolagem e outros enfeites. Se você deseja confirmação, tente aumentar a duração da operação de longa duração no violino (por exemplo, execute o loop externo mais 10 vezes) e você notará que, enquanto ele é executado, não é possível rolar a página. Se durar o suficiente, seu navegador perguntará se você deseja interromper o processo, porque está deixando a página sem resposta. O quadro está sendo executado, e o loop de eventos e a fila de mensagens ficam bloqueados até terminar.

Então, por que esse efeito colateral do texto não está sendo atualizado? Porque enquanto você ter alterado o valor do elemento no DOM - você pode console.log()seu valor imediatamente depois de mudar-lo e ver que ele tenha sido alterado (que mostra por que a explicação de DVK não é correto) - o navegador está aguardando a pilha para esgotam (a onfunção do manipulador para retornar) e, portanto, a mensagem para concluir, para que possa eventualmente executar a mensagem que foi adicionada pelo tempo de execução como uma reação à nossa operação de mutação e para refletir essa mutação na interface do usuário .

Isso ocorre porque, na verdade, estamos aguardando o código concluir a execução. Não dissemos "alguém busca isso e depois chama essa função com os resultados, obrigado, e agora estou pronto para retornar, faça o que for agora", como costumamos fazer com nosso Javascript assíncrono baseado em eventos. Entramos numa função de clique manipulador de eventos, nós atualizamos um elemento DOM, chamamos outra função, as outras obras de função por um longo tempo e, em seguida, retorna, nós, em seguida, atualizar o mesmo elemento DOM, e , em seguida, voltamos a partir da função inicial, efetivamente esvaziamento a pilha. E , em seguida, o navegador pode chegar ao próximo mensagem na fila, que pode muito bem ser uma mensagem gerada por nós, desencadeando algumas interna "on-DOM-mutação" tipo de evento.

A interface do usuário do navegador não pode (ou escolhe não) atualizar a interface do usuário até que o quadro atualmente em execução seja concluído (a função retornou). Pessoalmente, acho que isso é mais por design do que por restrição.

Por que a setTimeoutcoisa funciona então? Isso é feito porque remove efetivamente a chamada para a função de longa execução de seu próprio quadro, agendando para que seja executada posteriormente no windowcontexto, para que ele possa retornar imediatamente e permitir que a fila de mensagens processe outras mensagens. E a ideia é que a mensagem "na atualização" da interface do usuário que foi acionada por nós em Javascript ao alterar o texto no DOM agora esteja à frente da mensagem na fila para a função de longa duração, para que a atualização da interface do usuário aconteça antes de bloquearmos por muito tempo.

Observe que a) a função de execução demorada ainda bloqueia tudo quando é executada eb) você não tem garantia de que a atualização da interface do usuário esteja realmente à frente na fila de mensagens. No meu navegador Chrome de junho de 2018, um valor de 0"não corrige" o problema que o violino demonstra - 10 sim. Na verdade, sou um pouco sufocado por isso, porque me parece lógico que a mensagem de atualização da interface do usuário seja colocada na fila antes dela, pois seu gatilho é executado antes de agendar a função de longa execução para ser executada "mais tarde". Mas talvez haja algumas otimizações no mecanismo V8 que possam interferir, ou talvez meu entendimento esteja faltando.

Ok, então qual é o problema do uso setTimeoute qual a melhor solução para esse caso em particular?

Primeiro, o problema de usar setTimeoutqualquer manipulador de eventos como esse, para tentar aliviar outro problema, é propenso a mexer com outro código. Aqui está um exemplo da vida real do meu trabalho:

Um colega, em um entendimento mal informado sobre o loop de eventos, tentou "encadear" o Javascript usando algum código de renderização de modelo usado setTimeout 0para sua renderização. Ele não está mais aqui para perguntar, mas posso presumir que talvez ele tenha inserido temporizadores para medir a velocidade de renderização (que seria o retorno imediato das funções) e descobriu que usar essa abordagem resultaria em respostas incrivelmente rápidas dessa função.

O primeiro problema é óbvio; você não pode encadear o javascript, então você não ganha nada aqui enquanto adiciona ofuscação. Em segundo lugar, agora você separou efetivamente a renderização de um modelo da pilha de possíveis ouvintes de eventos que podem esperar que esse mesmo modelo tenha sido renderizado, enquanto pode muito bem não ter sido. O comportamento real dessa função agora não era determinista, como era - sem o saber - qualquer função que a executasse ou dependesse dela. Você pode fazer palpites, mas não pode codificar adequadamente seu comportamento.

A "correção" ao escrever um novo manipulador de eventos que dependia de sua lógica também era usar setTimeout 0. Mas isso não é uma correção, é difícil de entender e não é divertido depurar erros causados ​​por códigos como este. Às vezes, não há problema algum, outras vezes, falha de forma consistente e, novamente, às vezes funciona e quebra esporadicamente, dependendo do desempenho atual da plataforma e do que quer que aconteça no momento. É por isso que eu pessoalmente aconselho a não usar esse hack ( é um hack, e todos devemos saber que é), a menos que você realmente saiba o que está fazendo e quais são as consequências.

Mas o que pode nós fazer em vez disso? Bem, como o artigo mencionado no MDN sugere, divida o trabalho em várias mensagens (se você puder) para que outras mensagens enfileiradas possam ser intercaladas com o seu trabalho e executadas durante a execução ou use um trabalhador da Web, que pode executar em conjunto com sua página e retorne resultados quando terminar com seus cálculos.

Ah, e se você está pensando: "Bem, eu não poderia simplesmente colocar um retorno de chamada na função de longa duração para torná-lo assíncrono?", Então não. O retorno de chamada não o torna assíncrono, ainda será necessário executar o código de longa duração antes de chamar explicitamente o retorno de chamada.

DanielSmedegaardBuus
fonte
3

A outra coisa que isso faz é enviar a chamada de função para a parte inferior da pilha, impedindo o estouro da pilha se você estiver chamando uma função recursivamente. Isso tem o efeito de um whileloop, mas permite que o mecanismo JavaScript dispare outros cronômetros assíncronos.

Jason Suárez
fonte
Voto negativo para isso push the function invocation to the bottom of the stack. O que stackvocê está falando é obscuro. O mais importante é onde exatamente setTimeoutadiciona essa função, o final deste ciclo de loop ou o início do próximo ciclo de loop.
Verde
1

Ao chamar setTimeout, você dá tempo à página para reagir ao que quer que o usuário esteja fazendo. Isso é particularmente útil para funções executadas durante o carregamento da página.

Jeremy
fonte
1

Alguns outros casos em que setTimeout é útil:

Você deseja dividir um loop ou cálculo de longa duração em componentes menores para que o navegador não pare 'congelar' ou diga "O script na página está ocupado".

Você deseja desativar um botão de envio de formulário quando clicado, mas se você desativar o botão no manipulador onClick, o formulário não será enviado. setTimeout com um tempo de zero faz o truque, permitindo que o evento termine, o formulário comece a ser enviado e seu botão pode ser desativado.

fabspro
fonte
2
Desabilitar seria melhor realizado no evento onsubmit; seria mais rápido e é garantido que será chamado antes que o formulário seja tecnicamente enviado, pois você pode interromper o envio.
Kris
Muito verdadeiro. Suponho que a desativação ao clicar é mais fácil para a criação de protótipos, porque você pode simplesmente digitar onclick = "this.disabled = true" no botão, enquanto a desativação no envio requer um pouco mais de trabalho.
Fabspro #
1

As respostas sobre loops de execução e renderização do DOM antes da conclusão de outro código estão corretas. Tempo limite zero no JavaScript ajuda a tornar o código pseudo-multithread, mesmo que não seja.

Quero acrescentar que o valor BEST para um tempo limite de zero segundo de navegador / plataforma cruzada no JavaScript é na verdade cerca de 20 milissegundos em vez de 0 (zero), porque muitos navegadores móveis não podem registrar tempos limite menores que 20 milissegundos devido a limitações de relógio em chips AMD.

Além disso, os processos de execução demorada que não envolvem manipulação de DOM devem ser enviados aos Web Workers agora, pois fornecem verdadeira execução multithread do JavaScript.

ChrisN
fonte
2
Sou um pouco cético em relação à sua resposta, mas a votei positivamente porque me forçou a fazer algumas pesquisas adicionais sobre os padrões do navegador. Ao pesquisar padrões, vou para onde sempre vou, MDN: developer.mozilla.org/en-US/docs/Web/API/window.setTimeout A especificação HTML5 diz 4ms. Não diz nada sobre as limitações de clock em chips móveis. É difícil pesquisar no Google uma fonte de informações para fazer backup de suas declarações. Descobriu que o Dart Language do Google removeu completamente o setTimeout em favor de um objeto Timer.
John Zabroski
8
(...) porque muitos navegadores móveis não conseguem registrar tempos limites menores que 20 milissegundos devido a limitações de relógio (...) Toda plataforma tem limitações de tempo devido ao relógio e nenhuma plataforma é capaz de executar a próxima coisa exatamente 0ms após o atual. O tempo limite de 0ms solicita a execução de uma função o mais rápido possível e as limitações de tempo de uma plataforma específica não alteram o significado disso de forma alguma.
Piotr Dobrogost
1

setTimout em 0 também é muito útil no padrão de configuração de uma promessa adiada, que você deseja retornar imediatamente:

myObject.prototype.myMethodDeferred = function() {
    var deferredObject = $.Deferred();
    var that = this;  // Because setTimeout won't work right with this
    setTimeout(function() { 
        return myMethodActualWork.call(that, deferredObject);
    }, 0);
    return deferredObject.promise();
}
Stephan G
fonte
1

O problema era que você estava tentando executar uma operação Javascript em um elemento não existente. O elemento ainda estava para ser carregado e setTimeout()dá mais tempo para um elemento carregar das seguintes maneiras:

  1. setTimeout()faz com que o evento seja ansíncrono e, portanto, é executado após todo o código síncrono, dando ao seu elemento mais tempo para carregar. Retornos de chamada assíncronos como o retorno de chamada setTimeout()são colocados na fila de eventos e colocados na pilha pelo loop de eventos após a pilha de código síncrono estar vazia.
  2. O valor 0 para ms como um segundo argumento na função setTimeout()geralmente é um pouco mais alto (4-10ms, dependendo do navegador). Esse tempo ligeiramente mais alto necessário para executar os setTimeout()retornos de chamada é causado pela quantidade de 'ticks' (onde um tick pressiona um retorno de chamada na pilha se a pilha estiver vazia) do loop de eventos. Por motivos de desempenho e duração da bateria, a quantidade de tiques no loop de eventos é restrita a uma certa quantidade menor que 1000 vezes por segundo.
Willem van der Veen
fonte
-1

Javascript é um aplicativo de encadeamento único, para que não permita a execução simultânea de funções, de modo a alcançar esse evento. Então, exatamente o que setTimeout (fn, 0) faz com que sua busca por tarefas seja executada quando sua pilha de chamadas estiver vazia. Eu sei que essa explicação é muito chata, então eu recomendo que você analise este vídeo. Isso ajudará você a entender como as coisas funcionam sob o capô no navegador. Confira este vídeo: - https://www.youtube.com/watch?time_continue=392&v=8aGhZQkoFbQ

Jani Devang
fonte