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 selectedIndex
valor estaria fora de sincronia com o selecionado <option>
de index
atributo, como abaixo:
field.selectedIndex = element.index;
No entanto, esse código não estava funcionando. Mesmo que o campo selectedIndex
estivesse 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()
?
fonte
setTimeout(fn)
é o mesmo quesetTimeout(fn, 0)
, btw.Respostas:
Na pergunta, existia uma condição de corrida entre:
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:
Chamar
setTimeout
com 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.
fonte
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
onClick
manipulador do botão "fazer alguma coisa" chama uma função "LongCalc ()", que faz duas coisas:Faz um cálculo muito longo (digamos, leva 3 min)
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
onclick
manipulador (funçãoLongCalc()
) para fazer 4 coisas:Preencha o status "Calcular ... pode levar ~ 3 minutos" para o status DIV
Faz um cálculo muito longo (digamos, leva 3 min)
Imprime os resultados do cálculo na div resultado.
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:
[Empty]
[Execute OnClick handler(lines 1-4)]
[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 .[Execute OnClick handler(lines 3-4), re-draw Status DIV with "Calculating" value]
.[Execute OnClick handler(line 4), re-draw Status DIV with "Calculating" value, re-draw result DIV with result]
.[Execute OnClick handler, re-draw Status DIV with "Calculating" value, re-draw result DIV with result; re-draw Status DIV with "DONE" value]
.return
doonclick
manipulador sub. Retiramos o manipulador "Execute OnClick" da fila e começamos a executar o próximo item na fila.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 longasetTimeout
, você cria 2 eventos: asetTimeout
pró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
onClick
manipulador para DUAS instruções (em uma nova função ou apenas em um blocoonClick
):Preencha o status "Calcular ... pode levar ~ 3 minutos" para o status DIV
Execute
setTimeout()
com tempo limite 0 e uma chamada paraLongCalc()
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?
[Empty]
[Execute OnClick handler(status update, setTimeout() call)]
[Execute OnClick handler(which is a setTimeout call), re-draw Status DIV with new "Calculating" value]
.[re-draw Status DIV with "Calculating" value]
. A fila não tem novidades por mais 0 segundos.[re-draw Status DIV with "Calculating" value, execute LongCalc (lines 1-3)]
.[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:
Código JavaScript: (executado em
onDomReady
e pode exigir o jQuery 1.9)fonte
Dê uma olhada no artigo de John Resig sobre Como funcionam os temporizadores de JavaScript . Quando você define um tempo limite, ele na verdade enfileira o código assíncrono até que o mecanismo execute a pilha de chamadas atual.
fonte
setTimeout()
leva algum tempo até que os elementos DOM sejam carregados, mesmo se for definido como 0.Confira: setTimeout
fonte
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 issofn
ao 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.fonte
add this fn to the end of the queue
. O mais importante é onde exatamentesetTimeout
adiciona essa função, o final deste ciclo de loop ou o início do próximo ciclo de loop.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:
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:
A saída é:
fonte
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 dosetTimeout(fn, 0)
que vale a pena mencionar.fonte
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:
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
setTimeout
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 do HTML5 e os navegadores modernos o definem para 4 milissegundos.Também do mozilla:
As informações do PS são obtidas após a leitura do artigo a seguir .
fonte
setTimeout(fn,0)
ser útil.setTimeout
/setInterval
mais 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,setInterval
com o intervalo de 0 será reagendado imediatamente algumas vezes e depois será adiado por mais tempo), mas os usos simples desetTimeout
com aninhamento mínimo são permitidos na fila imediatamente.Como está sendo passado uma duração de
0
, suponho que seja para remover o código passado parasetTimeout
o 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.fonte
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
click
eventos ao evento no#do
botão.Em seguida, quando você realmente clica no botão,
message
é criado um referenciando a função de manipulador de eventos, que é adicionada aomessage queue
. Quandoevent loop
chega a essa mensagem, ele cria umframe
na 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 (aon
funçã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
setTimeout
coisa 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 nowindow
contexto, 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
setTimeout
e qual a melhor solução para esse caso em particular?Primeiro, o problema de usar
setTimeout
qualquer 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 0
para 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.
fonte
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
while
loop, mas permite que o mecanismo JavaScript dispare outros cronômetros assíncronos.fonte
push the function invocation to the bottom of the stack
. O questack
você está falando é obscuro. O mais importante é onde exatamentesetTimeout
adiciona essa função, o final deste ciclo de loop ou o início do próximo ciclo de loop.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.
fonte
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.
fonte
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.
fonte
setTimout em 0 também é muito útil no padrão de configuração de uma promessa adiada, que você deseja retornar imediatamente:
fonte
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: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 chamadasetTimeout()
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.setTimeout()
geralmente é um pouco mais alto (4-10ms, dependendo do navegador). Esse tempo ligeiramente mais alto necessário para executar ossetTimeout()
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.fonte
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
fonte