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

Recentemente encontrei um bug bastante desagradável, no qual o código carregava um dinamicamente via JavaScript. Este carregado dinamicamente tinha um valor pré-selecionado. No IE6, nós já tínhamos código para corrigir a selecionada, porque algumas vezes o valor selectedIndex do estaria fora de sincronia com o atributo index selecionado, como abaixo:

 field.selectedIndex = element.index; 

No entanto, esse código não estava funcionando. Mesmo que o selectedIndex do campo estivesse sendo configurado corretamente, o índice incorreto acabaria sendo selecionado. No entanto, se eu colocar uma instrução alert() no momento certo, a opção correta será selecionada. Pensando que isso pode ser algum tipo de problema de temporização, eu tentei algo random que eu vi no código antes:

 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 estou desconfortável por não saber exatamente por que isso resolve meu problema. Alguém tem uma explicação oficial? Qual problema de navegador estou evitando chamando minha function “mais tarde” usando setTimeout() ?

   

Isso funciona porque você está fazendo multi-tarefas cooperativas.

Um navegador tem que fazer várias coisas de uma só vez, e apenas uma delas é executar JavaScript. Mas uma das coisas que o JavaScript é usado com muita frequência é pedir ao navegador para criar um elemento de exibição. Isto é frequentemente assumido para ser feito de forma síncrona (particularmente como JavaScript não é executado em paralelo), mas não há garantia de que este é o caso e JavaScript não tem um mecanismo bem definido para espera.

A solução é “pausar” a execução do JavaScript para permitir que os threads de renderização sejam atualizados. E este é o efeito que setTimeout() com um timeout de 0 faz. É como um processo / thread em C. Embora pareça dizer “execute isso imediatamente”, ele realmente dá ao navegador uma chance de terminar de fazer algumas coisas não-JavaScript que estavam esperando para terminar antes de atender a essa nova peça de JavaScript .

(Na verdade, setTimeout() re-filas o novo JavaScript no final da fila de execução. Veja os comentários para links para uma explicação mais longa.)

IE6 só acontece a ser mais propenso a este erro, mas eu vi isso acontecer em versões mais antigas do Mozilla e no Firefox.


Veja Philip Roberts falar “O que diabos é o ciclo de events?” para uma explicação mais completa.

Prefácio:

NOTA IMPORTANTE: Embora seja mais votado e aceito, a resposta aceita pelo @staticsan, na verdade, NÃO ESTÁ CORRETA! – veja o comentário de David Mulder para explicar por quê.

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

Como tal, estou publicando uma visão detalhada do que o navegador faz e como o uso de setTimeout() ajuda . Parece longo, mas na verdade é muito simples e direto – eu apenas o fiz muito detalhado.

UPDATE: Eu fiz um JSFiddle para viver-demonstrar a explicação abaixo: http://jsfiddle.net/C2YBE/31/ . Muito obrigado a @ThangChung por ajudar a dar o pontapé inicial.

UPDATE2: Apenas no caso de JSFiddle morre, ou exclui o código, eu adicionei o código a esta resposta no final.


DETALHES :

Imagine um aplicativo da web com um botão “fazer algo” e um resultado div.

O manipulador onClick para o botão “fazer algo” chama uma function “LongCalc ()”, que faz duas coisas:

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

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

Agora, seus usuários começam a testar isso, clique em “fazer algo”, e a página fica lá aparentemente fazendo nada por 3 minutos, eles ficam inquietos, clicam no botão novamente, esperam 1 min, nada acontece, clicam 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 manipulador onclick (function LongCalc() ) para fazer 4 coisas:

  1. Preencher o status “Calculando … 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 no resultado div.

  4. Preencher o status “Cálculo concluído” no status DIV

E, felizmente, você dá ao aplicativo para os usuários fazerem um novo teste.

Eles voltam para você parecendo muito zangados. E explique que quando clicaram no botão, o Status DIV nunca foi atualizado com o status “Calculando …” !!!


Você coça a cabeça, pergunte pelo StackOverflow (ou leia documentos ou google) e perceba o problema:

O navegador coloca todas as tarefas “TODO” (tarefas da interface do usuário e comandos JavaScript) resultantes de events em uma única fila . E infelizmente, re-desenhar o DIV “Status” com o novo valor “Calculando …” é um TODO separado que vai para o final da fila!

Aqui está um detalhamento dos events 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 o 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] . Por favor, note que enquanto as mudanças DOM acontecem instantaneamente, para desenhar novamente o elemento DOM correspondente, você precisa de um novo evento, acionado pela alteração 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] [Execute OnClick handler, re-draw Status DIV with "Calculating" value, re-draw result DIV with result; re-draw Status DIV with "DONE" value] .
  • Evento: executa o return implícito do sub-rotina do manipulador onclick . Nós retiramos o manipulador “Execute OnClick” da fila e começamos a executar o próximo item na fila.
  • NOTA: Como já terminamos o cálculo, 3 minutos já passaram para o usuário. O evento re-draw não aconteceu ainda !!!
  • Evento: re-desenhe o Status DIV com o valor “Calculating”. Nós fazemos o re-sorteio e retiramos isso da fila.
  • Evento: re-draw Resultado DIV com valor do resultado. Nós fazemos o re-sorteio e retiramos isso da fila.
  • Evento: re-desenhe Status DIV com o valor “Concluído”. Nós fazemos o re-sorteio e retiramos isso da fila. Os espectadores de olhos agudos podem até notar “Status DIV com valor” Calculando “piscando por fração de microssegundo – DEPOIS DO CÁLCULO FINALIZADO

Então, o problema subjacente é que o evento de re-draw para “Status” DIV é colocado na fila no final, APÓS o evento “execute linha 2” que leva 3 minutos, então o re-draw real não acontece até APÓS o cálculo ser feito.


Para o resgate vem o setTimeout() . Como isso ajuda? Porque, chamando código de execução longa via setTimeout , você cria dois events: a execução setTimeout , e (devido a 0 timeout), separa a input da fila para o código que está sendo executado.

Então, para consertar seu problema, você modifica seu manipulador onClick para ser duas instruções (em uma nova function ou apenas um bloco dentro do onClick ):

  1. Preencher o status “Calculando … pode levar ~ 3 minutos” para o status DIV

  2. Execute setTimeout() com 0 timeout e uma chamada para a function LongCalc() .

    LongCalc() function LongCalc() é quase a mesma da última vez, mas obviamente não tem a atualização DIV “Calculating …” como primeiro passo; e, em vez disso, inicia o cálculo imediatamente.

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

  • Fila: [Empty]
  • Evento: clique no botão. Fila após o 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 nada de novo nela por mais 0 segundos.
  • Evento: O alarme do tempo limite é desativado, 0 segundos depois. Fila após: [re-draw Status DIV with "Calculating" value, execute LongCalc (lines 1-3)] .
  • Evento: re-desenhe o Status DIV com o valor “Calculating” . Fila depois: [execute LongCalc (lines 1-3)] . Por favor, note que este evento de re-draw pode realmente acontecer ANTES de o alarme triggersr, o que funciona igualmente bem.

Viva! O Status DIV acabou de ser atualizado para “Calculando …” antes do cálculo ser iniciado !!!



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

Código HTML:

 
Not Calculating yet.
Not Calculating yet.

Código JavaScript: (Executado em onDomReady e pode exigir 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); }); 

Dê uma olhada no artigo de John Resig sobre Como funcionam os cronômetros JavaScript . Quando você define um tempo limite, ele realmente coloca em fila o código asynchronous até que o mecanismo execute a pilha de chamadas atual.

A maioria dos navegadores tem um processo chamado thread principal, que é responsável por executar algumas tarefas JavaScript, atualizações de UI, por exemplo: pintura, redesenho ou restream, etc.

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

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

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

setTimeout() compra algum tempo até que os elementos DOM sejam carregados, mesmo que esteja definido como 0.

Verifique isso: setTimeout

Há aqui respostas conflitantes e sem provas, não há como saber em quem acreditar. Aqui está a prova de que o @DVK está correto e o @SalvadorDali está errado. Este último afirma:

“E aqui está o porquê: não é possível ter 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 para 10 milissegundos, mas as especificações e os 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 empurra a function de retorno de chamada para o final da fila de execução. Se depois de setTimeout (callback, 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 vários segundos, até que o código de bloqueio tenha terminado. Experimente 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 

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

edite agora que é 2015 eu deveria notar que há também requestAnimationFrame() , que não é exatamente o mesmo, mas é suficientemente próximo de setTimeout(fn, 0) que vale a pena mencionar.

Como está sendo passado uma duração de 0 , suponho que seja para remover o código passado para o setTimeout do stream de execução. Então, se é uma function que pode demorar um pouco, não impedirá que o código subseqüente seja executado.

Esta é uma pergunta antiga com respostas antigas. Eu queria adicionar um novo olhar a 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, em seguida, chame-os na seguinte ordem f1(); f2(); f1(); f2(); só para ver que o segundo foi executado primeiro.

E aqui está o porquê: não é possível ter 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 para 10 milissegundos, mas as especificações HTML5 e os navegadores modernos definem quatro 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 .

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

A outra coisa que isso faz é empurrar a chamada de function para a parte inferior da pilha, evitando um estouro de pilha se você estiver chamando uma function recursivamente. Isso tem o efeito de um loop while, mas permite que o mecanismo JavaScript dispare outros timers asynchronouss.

As respostas sobre loops de execução e renderização do DOM antes de algum outro código ser concluído estão corretas. Zero segundos tempos limite em JavaScript ajudam 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 cross-browser / cross-platform em JavaScript é 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 clock em chips AMD.

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

Ao chamar setTimeout, você dá o tempo da 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.

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ça “congelar” ou dizer “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 enviar, então o seu botão pode ser desativado.

setTimout em 0 também é muito útil no padrão de configuração de uma promise 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(); } 

Ambas as duas respostas mais bem avaliadas estão erradas. Verifique a descrição do MDN no modelo de simultaneidade e no loop de events e deve ficar claro o que está acontecendo (esse recurso do MDN é uma verdadeira joia). E simplesmente usar setTimeout pode estar adicionando problemas inesperados em seu código, além de “resolver” esse pequeno problema.

O que realmente está acontecendo aqui não é que “o navegador pode não estar pronto ainda porque a simultaneidade”, ou algo baseado em “cada linha é um evento que é adicionado ao final da fila”.

O jsfiddle fornecido por DVK na verdade ilustra um problema, mas sua explicação para isso não está correta.

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

Então, quando você realmente clica no botão, uma message é criada referenciando a function do manipulador de events, que é adicionada à message queue . Quando o event loop atinge essa mensagem, ele cria um frame na pilha, com a chamada de function para o manipulador de events click no jsfiddle.

E é aí que fica interessante. Estamos tão acostumados a pensar no JavaScript como sendo asynchronous que estamos propensos a ignorar esse fato minúsculo: Qualquer quadro deve ser executado, na íntegra, antes que o próximo quadro possa ser executado . Sem concorrência, pessoas.

O que isto significa? Isso significa que sempre que uma function é invocada a partir da fila de mensagens, ela bloqueia a fila até que a pilha que ela gera tenha sido esvaziada. Ou, em termos mais gerais, bloqueia até que a function retorne. E bloqueia tudo , incluindo as operações de renderização do DOM, rolagem e outras coisas. Se você quiser 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ê perceberá que, enquanto estiver em execução, não será possível rolar a página. Se a execução for longa o suficiente, o seu navegador perguntará se você quer matar o processo, porque está deixando a página sem resposta. O quadro está sendo executado e o loop de events e a fila de mensagens estão presos até que seja finalizado.

Então, por que esse efeito colateral do texto não está atualizando? Porque enquanto você alterou o valor do elemento no DOM – você pode console.log() seu valor imediatamente após alterá-lo e ver que ele foi alterado (o que mostra porque a explicação do DVK não está correta) – o navegador está esperando para a pilha esgotar (a function on handler para retornar) e, portanto, a mensagem terminar, para que ela 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 é porque estamos realmente esperando que o código termine de ser executado. Nós não dissemos “alguém busque isso e então chame essa function com os resultados, obrigado, e agora estou pronto para que eu volte, faça o que quiser agora”, como normalmente fazemos com nosso JavaScript asynchronous baseado em events. Entramos em uma function manipuladora de events click, nós atualizamos um elemento DOM, chamamos outra function, a outra function trabalha por um longo tempo e então retorna, nós então atualizamos o mesmo elemento DOM, e então retornamos da function inicial, efetivamente esvaziando a pilha. E, em seguida, o navegador pode chegar à próxima mensagem na fila, o que pode muito bem ser uma mensagem gerada por nós, acionando algum tipo de evento “on-DOM-mutation” interno.

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

Por que a coisa setTimeout funciona então? Ele faz isso, porque efetivamente remove a chamada para a function de longa execução de seu próprio quadro, agendando-a para ser executada posteriormente no contexto da window , para que ela mesma possa retornar imediatamente e permitir que a fila de mensagens processe outras mensagens. E a idéia é 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 está agora à frente da mensagem enfileirada para a function de longa execução, para que a atualização da interface aconteça antes de bloquear por muito tempo.

Observe que a) A function de execução demorada ainda bloqueia tudo quando é executada e b) não se garante que a atualização da interface do usuário esteja realmente à frente na fila de mensagens. On my June 2018 Chrome browser, a value of 0 does not “fix” the problem the fiddle demonstrates — 10 does. I’m actually a bit stifled by this, because it seems logical to me that the UI update message should be queued up before it, since its trigger is executed before scheduling the long-running function to be run “later”. But perhaps there’re some optimisations in the V8 engine that may interfere, or maybe my understanding is just lacking.

Okay, so what’s the problem with using setTimeout , and what’s a better solution for this particular case?

First off, the problem with using setTimeout on any event handler like this, to try to alleviate another problem, is prone to mess with other code. Here’s a real-life example from my work:

A colleague, in a mis-informed understanding on the event loop, tried to “thread” Javascript by having some template rendering code use setTimeout 0 for its rendering. He’s no longer here to ask, but I can presume that perhaps he inserted timers to gauge the rendering speed (which would be the return immediacy of functions) and found that using this approach would make for blisteringly fast responses from that function.

First problem is obvious; you cannot thread javascript, so you win nothing here while you add obfuscation. Secondly, you have now effectively detached the rendering of a template from the stack of possible event listeners that might expect that very template to have been rendered, while it may very well not have been. The actual behaviour of that function was now non-deterministic, as was — unknowingly so — any function that would run it, or depend on it. You can make educated guesses, but you cannot properly code for its behaviour.

The “fix” when writing a new event handler that depended on its logic was to also use setTimeout 0 . But, that’s not a fix, it is hard to understand, and it is no fun to debug errors that are caused by code like this. Sometimes there’s no problem ever, other times it concistently fails, and then again, sometimes it works and breaks sporadically, depending on the current performance of the platform and whatever else happens to going on at the time. This is why I personally would advise against using this hack (it is a hack, and we should all know that it is), unless you really know what you’re doing and what the consequences are.

But what can we do instead? Well, as the referenced MDN article suggests, either split the work into multiple messages (if you can) so that other messages that are queued up may be interleaved with your work and executed while it runs, or use a web worker, which can run in tandem with your page and return results when done with its calculations.

Oh, and if you’re thinking, “Well, couldn’t I just put a callback in the long-running function to make it asynchronous?,” then no. The callback doesn’t make it asynchronous, it’ll still have to run the long-running code before explicitly calling your callback.

Javascript is single threaded application so that don’t allow to run function concurrently so to achieve this event loops are use. So exactly what setTimeout(fn, 0) do that its pussed into task quest which is executed when your call stack is empty. I know this explanation is pretty boring, so i recommend you to go through this video this will help you how things work under the hood in browser. Check out this video:- https://www.youtube.com/watch?time_continue=392&v=8aGhZQkoFbQ