Perché setTimeout (fn, 0) a volte è utile?


872

Di recente ho riscontrato un bug piuttosto sgradevole, in cui il codice stava caricando in modo <select>dinamico tramite JavaScript. Questo carico dinamico <select>aveva un valore preselezionato. In IE6, abbiamo già avuto il codice per risolvere il selezionato <option>, perché a volte il <select>'s selectedIndexvalore sarebbe fuori sincronia con l'selezionate <option>' s indexattributo, come di seguito:

field.selectedIndex = element.index;

Tuttavia, questo codice non funzionava. Anche se il campo selectedIndexveniva impostato correttamente, l'indice sbagliato sarebbe stato selezionato. Tuttavia, se inserissi una alert()dichiarazione al momento giusto, verrebbe selezionata l'opzione corretta. Pensando che questo potrebbe essere una sorta di problema di temporizzazione, ho provato qualcosa di casuale che avevo visto prima nel codice:

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

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

E questo ha funzionato!

Ho una soluzione per il mio problema, ma sono inquieto di non sapere esattamente perché questo risolva il mio problema. Qualcuno ha una spiegazione ufficiale? Quale problema del browser sto evitando chiamando la mia funzione "più tardi" utilizzando setTimeout()?


2
La maggior parte della domanda descrive perché è utile. Se hai bisogno di sapere perché questo accade - leggi la mia risposta: stackoverflow.com/a/23747597/1090562
Salvador Dali,

18
Philip Roberts lo spiega nel migliore dei modi qui nel suo discorso "Che diamine è il circuito degli eventi?" youtube.com/watch?v=8aGhZQkoFbQ
vasa

Se hai fretta questa è la parte del video che inizia a rispondere esattamente alla domanda: youtu.be/8aGhZQkoFbQ?t=14m54s . A prescindere da ciò, l'intero video merita sicuramente una visione.
William Hampshire,

4
setTimeout(fn)è lo stesso di setTimeout(fn, 0)btw.
vsync,

Risposte:


827

Nella domanda, esisteva una condizione di competizione tra:

  1. Il tentativo del browser di inizializzare l'elenco a discesa, pronto per l'aggiornamento dell'indice selezionato e
  2. Il tuo codice per impostare l'indice selezionato

Il tuo codice ha costantemente vinto questa gara e ha tentato di impostare la selezione a discesa prima che il browser fosse pronto, il che significa che il bug appariva.

Questa razza esisteva perché JavaScript ha un singolo thread di esecuzione condiviso con il rendering della pagina. In effetti, l'esecuzione di JavaScript blocca l'aggiornamento del DOM.

La soluzione alternativa era:

setTimeout(callback, 0)

Richiamando setTimeoutcon un callback e zero come secondo argomento, verrà pianificato l'esecuzione del callback modo asincrono , dopo il minor ritardo possibile - che sarà di circa 10 ms quando la scheda ha lo stato attivo e il thread di esecuzione JavaScript non è occupato.

La soluzione del PO, quindi, era di ritardare di circa 10ms, l'impostazione dell'indice selezionato. Ciò ha dato al browser l'opportunità di inizializzare il DOM, risolvendo il bug.

Ogni versione di Internet Explorer presentava comportamenti bizzarri e questo tipo di soluzione alternativa era talvolta necessaria. In alternativa potrebbe essere stato un vero e proprio bug nella base di codice del PO.


Vedi Philip Roberts parlare "Che diamine è il circuito degli eventi?" per una spiegazione più approfondita.


276
"La soluzione è" mettere in pausa "l'esecuzione di JavaScript per consentire ai thread di rendering di raggiungere." Non del tutto vero, ciò che setTimeout fa è aggiungere un nuovo evento alla coda degli eventi del browser e il motore di rendering è già in quella coda (non del tutto vero, ma abbastanza vicino) in modo che venga eseguito prima dell'evento setTimeout.
David Mulder,

48
Sì, questa è una risposta molto più dettagliata e molto più corretta. Ma il mio è "abbastanza corretto" perché le persone capiscano perché il trucco funziona.
staticsan

2
@DavidMulder, significa che il browser analizza i CSS e il rendering in un thread diverso dal thread di esecuzione javascript?
Jason

8
No, sono analizzati in linea di principio nello stesso thread, altrimenti alcune righe di manipolazione del DOM innescherebbero reflow in continuazione, il che avrebbe un'influenza estremamente negativa sulla velocità di esecuzione.
David Mulder,

30
Questo video è la migliore spiegazione al perché di noi setTimeout 0 2014.jsconf.eu/speakers/…
davibq

665

Prefazione:

Alcune delle altre risposte sono corrette ma in realtà non illustrano quale sia il problema da risolvere, quindi ho creato questa risposta per presentare quell'illustrazione dettagliata.

In quanto tale, sto pubblicando una panoramica dettagliata di ciò che fa il browser e di come l'utilizzo setTimeout()aiuta . Sembra lungo ma in realtà è molto semplice e diretto - l'ho appena reso molto dettagliato.

AGGIORNAMENTO: ho creato un JSFiddle per dimostrare dal vivo la seguente spiegazione: http://jsfiddle.net/C2YBE/31/ . Molte grazie a @ThangChung per aver contribuito a kickstart.

UPDATE2: Nel caso in cui il sito Web JSFiddle dovesse morire o elimini il codice, alla fine ho aggiunto il codice a questa risposta.


DETTAGLI :

Immagina un'app Web con un pulsante "fai qualcosa" e un div di risultato.

Il onClickgestore del pulsante "fai qualcosa" chiama una funzione "LongCalc ()", che fa 2 cose:

  1. Fa un calcolo molto lungo (diciamo impiega 3 minuti)

  2. Stampa i risultati del calcolo nel div risultato.

Ora, i tuoi utenti iniziano a testarlo, fai clic sul pulsante "fai qualcosa" e la pagina rimane lì apparentemente senza fare nulla per 3 minuti, diventano irrequieti, fai di nuovo clic sul pulsante, attendi 1 minuto, non succede nulla, fai di nuovo clic sul pulsante ...

Il problema è ovvio: vuoi un DIV "Status", che mostra cosa sta succedendo. Vediamo come funziona.


Quindi aggiungi un DIV "Status" (inizialmente vuoto) e modifichi il onclickgestore (funzione LongCalc()) per fare 4 cose:

  1. Popolare lo stato "Calcolo ... potrebbe richiedere ~ 3 minuti" nello stato DIV

  2. Fa un calcolo molto lungo (diciamo impiega 3 minuti)

  3. Stampa i risultati del calcolo nel div risultato.

  4. Popolare lo stato "Calcolo eseguito" nello stato DIV

E dai felicemente l'app agli utenti per riprovare.

Tornano da te molto arrabbiati. E spiega che quando hanno fatto clic sul pulsante, lo stato DIV non è mai stato aggiornato con lo stato "Calcolo ..." !!!


Ti gratti la testa, chiedi in giro su StackOverflow (o leggi documenti o google) e realizzi il problema:

Il browser inserisce tutte le attività "TODO" (sia le attività dell'interfaccia utente sia i comandi JavaScript) risultanti dagli eventi in una singola coda . E sfortunatamente, ridisegnare il DIV "Stato" con il nuovo valore "Calcolo in corso ..." è un TODO separato che va alla fine della coda!

Ecco una ripartizione degli eventi durante il test dell'utente, i contenuti della coda dopo ogni evento:

  • Coda: [Empty]
  • Evento: fare clic sul pulsante. Coda dopo evento:[Execute OnClick handler(lines 1-4)]
  • Evento: eseguire la prima riga nel gestore OnClick (ad es. Modificare il valore DIV dello stato). Coda dopo evento: [Execute OnClick handler(lines 2-4), re-draw Status DIV with new "Calculating" value]. Si noti che mentre le modifiche al DOM avvengono istantaneamente, per ridisegnare l'elemento DOM corrispondente è necessario un nuovo evento, attivato dalla modifica del DOM, che è andato alla fine della coda .
  • PROBLEMA!!! PROBLEMA!!! Dettagli spiegati di seguito.
  • Evento: eseguire la seconda riga nel gestore (calcolo). Coda dopo: [Execute OnClick handler(lines 3-4), re-draw Status DIV with "Calculating" value].
  • Evento: eseguire la terza riga nel gestore (compilare il risultato DIV). Coda dopo: [Execute OnClick handler(line 4), re-draw Status DIV with "Calculating" value, re-draw result DIV with result].
  • Evento: eseguire la quarta riga nel gestore (compilare lo stato DIV con "FATTO"). Coda: [Execute OnClick handler, re-draw Status DIV with "Calculating" value, re-draw result DIV with result; re-draw Status DIV with "DONE" value].
  • Evento: eseguire implicito returndal onclicksub gestore. Eliminiamo il "gestore OnClick Execute" dalla coda e iniziamo a eseguire l'elemento successivo nella coda.
  • NOTA: poiché abbiamo già terminato il calcolo, sono già trascorsi 3 minuti per l'utente. L'evento di re-draw non è ancora accaduto !!!
  • Evento: ridisegnare lo stato DIV con il valore "Calcolo". Facciamo il re-draw e lo togliiamo dalla coda.
  • Evento: ridisegna risultato DIV con valore risultato. Facciamo il re-draw e lo togliiamo dalla coda.
  • Evento: ridisegna lo stato DIV con il valore "Fatto". Facciamo il re-draw e lo togliiamo dalla coda. Gli spettatori con gli occhi acuti potrebbero persino notare "Status DIV con il valore" Calcolo "lampeggiante per la frazione di un microsecondo - DOPO IL CALCOLO FINITO

Quindi, il problema di fondo è che l'evento di re-draw per DIV "Status" viene inserito nella coda alla fine, DOPO l'evento "execute line 2" che richiede 3 minuti, quindi l'effettiva re-draw non si verifica fino a quando DOPO che il calcolo è stato effettuato.


In soccorso arriva il setTimeout(). Come aiuta Perché chiamando il codice a esecuzione prolungata tramite setTimeout, in realtà si creano 2 eventi:setTimeout esecuzione stessa e (a causa del timeout 0), voce di coda separata per il codice in esecuzione.

Quindi, per risolvere il tuo problema, modifica il onClickgestore in modo che diventi DUE istruzioni (in una nuova funzione o solo un blocco all'interno onClick):

  1. Popolare lo stato "Calcolo ... potrebbe richiedere ~ 3 minuti" nello stato DIV

  2. Eseguire setTimeout()con 0 timeout e una chiamata alla LongCalc()funzione .

    LongCalc()la funzione è quasi uguale all'ultima volta ma ovviamente non ha lo stato "Calcolo in corso ..." Aggiornamento DIV come primo passo; e invece avvia subito il calcolo.

Quindi, che aspetto hanno la sequenza degli eventi e la coda?

  • Coda: [Empty]
  • Evento: fare clic sul pulsante. Coda dopo evento:[Execute OnClick handler(status update, setTimeout() call)]
  • Evento: eseguire la prima riga nel gestore OnClick (ad es. Modificare il valore DIV dello stato). Coda dopo evento: [Execute OnClick handler(which is a setTimeout call), re-draw Status DIV with new "Calculating" value].
  • Evento: eseguire la seconda riga nel gestore (chiamata setTimeout). Coda dopo: [re-draw Status DIV with "Calculating" value]. La coda non contiene nulla di nuovo per altri 0 secondi.
  • Evento: l'allarme dal timeout si spegne, 0 secondi dopo. Coda dopo: [re-draw Status DIV with "Calculating" value, execute LongCalc (lines 1-3)].
  • Evento: ridisegnare lo stato DIV con il valore "Calcolo" . Coda dopo: [execute LongCalc (lines 1-3)]. Si prega di notare che questo evento di riprogrammazione potrebbe effettivamente accadere PRIMA che l'allarme si attivi, il che funziona altrettanto bene.
  • ...

Evviva! Lo stato DIV è appena stato aggiornato in "Calcolo in corso ..." prima dell'inizio del calcolo !!!



Di seguito è riportato il codice di esempio dalla JSFiddle che illustra questi esempi: http://jsfiddle.net/C2YBE/31/ :

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

Codice JavaScript: (eseguito su onDomReadye potrebbe richiedere 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);
});

12
ottima risposta DVK! Ecco un esempio che illustra il tuo esempio gist.github.com/kumikoda/5552511#file-timeout-html
kumikoda

4
Risposta davvero interessante, DVK. Giusto per immaginare, ho messo quel codice su jsfiddle jsfiddle.net/thangchung/LVAaV
thangchung l'

4
@ThangChung - Ho provato a creare una versione migliore (2 pulsanti, uno per ogni caso) in JSFiddle. Funziona come una demo su Chrome e IE ma non su FF per qualche motivo - vedi jsfiddle.net/C2YBE/31 . Ho chiesto perché FF non funziona qui: stackoverflow.com/questions/20747591/…
DVK

1
@DVK "Il browser inserisce tutte le sue attività" TODO "(sia attività UI che comandi JavaScript) risultanti da eventi in una singola coda". Signore, per favore, può fornire la fonte per questo? Imho arent browser dovrebbero avere UI (rendering engine) e thread JS diversi ... nessuna offesa intesa .... voglio solo imparare ..
bhavya_w,

1
@bhavya_w No, tutto accade su un thread. Questo è il motivo per cui un lungo calcolo js può bloccare l'interfaccia utente
Seba Kerckhof,

88

Dai un'occhiata all'articolo di John Resig su come funzionano i timer JavaScript . Quando si imposta un timeout, in realtà il codice asincrono viene messo in coda fino a quando il motore non esegue lo stack di chiamate corrente.


23

setTimeout() ti offre un po 'di tempo prima che gli elementi DOM siano caricati, anche se è impostato su 0.

Dai un'occhiata: setTimeout


23

La maggior parte dei browser ha un processo chiamato thread principale, responsabile dell'esecuzione di alcune attività JavaScript, aggiornamenti dell'interfaccia utente, ad esempio: disegno, ridisegna o ridisposizione, ecc.

Alcune attività di esecuzione JavaScript e di aggiornamento dell'interfaccia utente vengono messe in coda nella coda dei messaggi del browser, quindi vengono inviate al thread principale del browser da eseguire.

Quando gli aggiornamenti dell'interfaccia utente vengono generati mentre il thread principale è occupato, le attività vengono aggiunte nella coda dei messaggi.

setTimeout(fn, 0);aggiungilo fnalla fine della coda da eseguire. Pianifica un'attività da aggiungere sulla coda dei messaggi dopo un determinato periodo di tempo.


"Ogni esecuzione di JavaScript e attività di aggiornamento dell'interfaccia utente vengono aggiunte al sistema della coda degli eventi del browser, quindi tali attività vengono inviate al thread dell'interfaccia utente principale del browser per essere eseguite." .... fonte per favore?
bhavya_w,

4
JavaScript ad alte prestazioni (Nicholas Zakas, Stoyan Stefanov, Ross Harmes, Julien Lecomte e Matt Sweeney)
Arley,

Downvote per questo add this fn to the end of the queue. Il più importante è dove setTimeoutaggiunge esattamente questa funzione, la fine di questo ciclo di loop o l'inizio del ciclo di loop successivo.
Green

19

Ci sono risposte contraddittorie contrastanti qui, e senza prove non c'è modo di sapere a chi credere. Ecco la prova che @DVK ha ragione e @SalvadorDali non è corretto. Quest'ultimo afferma:

"Ed ecco perché: non è possibile avere setTimeout con un ritardo di 0 millisecondi. Il valore minimo è determinato dal browser e non è 0 millisecondi. Storicamente i browser impostano questo minimo su 10 millisecondi, ma le specifiche HTML5 e i browser moderni lo hanno impostato a 4 millisecondi. "

Il timeout minimo di 4 ms è irrilevante per ciò che sta accadendo. Quello che succede davvero è che setTimeout spinge la funzione di callback alla fine della coda di esecuzione. Se dopo setTimeout (callback, 0) si dispone di un codice di blocco che richiede alcuni secondi per essere eseguito, il callback non verrà eseguito per alcuni secondi, fino al termine del codice di blocco. Prova questo codice:

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

L'output è:

setting timeout 0 callback at 0
starting blocking loop at 5
blocking loop ended at 3000
in timeout callback at 3033

la tua risposta non dimostra nulla. Mostra solo che sul tuo computer in una situazione specifica il computer ti lancia alcuni numeri. Per dimostrare qualcosa di correlato hai bisogno di un po 'più di poche righe di codice e pochi numeri.
Salvador Dali,

6
@SalvadorDali, credo che la mia prova sia abbastanza chiara per la maggior parte delle persone a capire. Penso che ti senti sulla difensiva e non hai fatto lo sforzo di capirlo. Sarò felice di tentare di chiarirlo, ma non so cosa non riesci a capire. Prova a eseguire il codice sul tuo computer se sospetti i miei risultati.
Vladimir Kornea,

Proverò a chiarire per l'ultima volta. La mia risposta sta chiarendo alcune delle proprietà interessanti in timeout, intervallo ed è ben supportata da fonti valide e riconoscibili. Hai affermato con forza che è sbagliato e hai indicato il tuo codice che per qualche motivo hai chiamato una prova. Non c'è niente di sbagliato nel tuo pezzo di codice (non sono contrario). Sto solo dicendo che la mia risposta non è sbagliata. Chiarisce alcuni punti. Sto per fermare questa guerra, perché non riesco a vedere un punto.
Salvador Dali,

15

Un motivo per farlo è quello di rinviare l'esecuzione del codice a un ciclo di eventi successivo separato. Quando si risponde a un evento del browser di qualche tipo (clic del mouse, ad esempio), a volte è necessario eseguire operazioni solo dopo l'elaborazione dell'evento corrente. La setTimeout()struttura è il modo più semplice per farlo.

modifica ora che è il 2015 dovrei notare che c'è anche requestAnimationFrame(), che non è esattamente lo stesso, ma è abbastanza vicino a setTimeout(fn, 0)quello che vale la pena menzionare.


Questo è precisamente uno dei luoghi in cui l'ho visto essere utilizzato. =)
Zaibot,

FYI: questa risposta è stata incorporata da qui stackoverflow.com/questions/4574940/...
Shog9

2
è rinviare l'esecuzione del codice a un ciclo di eventi successivo separato : come si fa a capire un ciclo di eventi successivo ? Come capisci cos'è un loop di eventi attuale? Come fai a sapere in quale giro di eventi sei ora?
Verde,

@Verde, no, davvero; non c'è davvero alcuna visibilità diretta su ciò che il runtime JavaScript sta facendo.
Punta il

requestAnimationFrame ha risolto il problema che avevo con IE e Firefox non aggiornando l'interfaccia utente a volte
David

9

Questa è una vecchia domanda con vecchie risposte. Volevo aggiungere una nuova occhiata a questo problema e rispondere al perché questo accade e non al perché sia ​​utile.

Quindi hai due funzioni:

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

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

e poi chiamali nel seguente ordine f1(); f2();solo per vedere che il secondo è stato eseguito per primo.

Ed ecco perché: non è possibile avere setTimeoutcon un ritardo di 0 millisecondi. Il valore minimo è determinato dal browser e non è 0 millisecondi. Storicamente i browser impostano questo minimo su 10 millisecondi, ma le specifiche HTML5 e i browser moderni lo hanno impostato su 4 millisecondi.

Se il livello di annidamento è maggiore di 5 e il timeout è inferiore a 4, aumentare il timeout a 4.

Anche da Mozilla:

Per implementare un timeout di 0 ms in un browser moderno, è possibile utilizzare window.postMessage () come descritto qui .

Le informazioni PS vengono prese dopo aver letto il seguente articolo .


1
@ user2407309 Stai scherzando? vuoi dire che la specifica HTML5 è sbagliata e hai ragione? Leggi le fonti prima di sottoporre a voto negativo e fare forti affermazioni. La mia risposta si basa su specifiche HTML e record storici. Invece di fare la risposta che spiega completamente le stesse cose ancora e ancora, ho aggiunto qualcosa di nuovo, qualcosa che non era mostrato nelle risposte precedenti. Non sto dicendo che questa è l'unica ragione, sto solo mostrando qualcosa di nuovo.
Salvador Dali,

1
Questo non è corretto: "Ed ecco perché: non è possibile avere setTimeout con un ritardo di 0 millisecondi." Non è per questo. Il ritardo di 4 ms è irrilevante per il motivo per cui setTimeout(fn,0)è utile.
Vladimir Kornea,

@ user2407309 può essere facilmente modificato in "per aggiungere ai motivi indicati da altri, non è possibile ....". Quindi è ridicolo sottovalutare proprio per questo, specialmente se la tua risposta non dice nulla di nuovo. Basta una piccola modifica.
Salvador Dali,

10
Salvador Dalì: Se ignori gli aspetti emotivi della guerra delle micro fiamme qui, probabilmente dovresti ammettere che @VladimirKornea ha ragione. È vero che i browser associano un ritardo di 0 ms a 4 ms, ma anche se non lo facessero, i risultati sarebbero sempre gli stessi. Il meccanismo di guida qui è che il codice viene inserito nella coda, piuttosto che nello stack di chiamate. Dai un'occhiata a questa eccellente presentazione di JSConf, che potrebbe aiutare a chiarire il problema: youtube.com/watch?v=8aGhZQkoFbQ
hashchange

1
Sono confuso sul perché pensi che il tuo preventivo su un minimo qualificato di 4 ms sia un minimo globale di 4 ms. Come mostra la tua citazione dalle specifiche HTML5, il minimo è di 4 ms solo quando hai annidato le chiamate a setTimeout/ setIntervalpiù di cinque livelli di profondità; se non lo hai fatto, il minimo è 0 ms (cosa con mancanza di macchine del tempo). I documenti di Mozilla espandono questo per coprire i casi ripetuti, non solo nidificati (quindi setIntervalcon intervallo di 0 riprogrammeranno immediatamente alcune volte, quindi ritarderanno più a lungo dopo), ma gli usi semplici setTimeoutcon annidamento minimo possono fare immediatamente la coda.
ShadowRanger,

8

Dal momento che viene passato per una durata di 0, suppongo che sia per rimuovere il codice passato al setTimeoutdal flusso di esecuzione. Quindi, se è una funzione che potrebbe richiedere del tempo, non impedirà l'esecuzione del codice successivo.


FYI: questa risposta è stata incorporata da qui stackoverflow.com/questions/4574940/...
Shog9

7

Entrambe queste due risposte migliori sono sbagliate. Controlla la descrizione MDN sul modello di concorrenza e sul ciclo degli eventi e dovrebbe essere chiaro cosa sta succedendo (quella risorsa MDN è un vero gioiello). E semplicemente usando setTimeout può essere aggiungere problemi imprevisti nel codice oltre a "risolvere" questo piccolo problema.

Ciò che sta realmente accadendo qui non è che "il browser potrebbe non essere ancora del tutto pronto perché la concorrenza" o qualcosa basato su "ogni riga è un evento che viene aggiunto al retro della coda".

Il jsfiddle fornito da DVK in effetti illustra un problema, ma la sua spiegazione non è corretta.

Quello che sta succedendo nel suo codice è che per prima cosa sta collegando un gestore di eventi clickall'evento sul #dopulsante.

Quindi, quando si fa effettivamente clic sul pulsante, messageviene creato un riferimento alla funzione del gestore eventi, che viene aggiunta a message queue. Quando event loopraggiunge questo messaggio, viene creato uno framestack, con la chiamata di funzione al gestore eventi click nel jsfiddle.

Ed è qui che diventa interessante. Siamo così abituati a pensare a Javascript come asincrono che siamo inclini a trascurare questo piccolo fatto: ogni fotogramma deve essere eseguito, per intero, prima che il fotogramma successivo possa essere eseguito . Nessuna concorrenza, persone.

Cosa significa questo? Significa che ogni volta che una funzione viene invocata dalla coda dei messaggi, blocca la coda fino allo svuotamento dello stack che genera. Oppure, in termini più generali, si blocca fino a quando non viene restituita la funzione. E blocca tutto , comprese le operazioni di rendering DOM, lo scorrimento e quant'altro. Se si desidera la conferma, provare ad aumentare la durata dell'operazione di esecuzione prolungata nel violino (ad esempio, eseguire il loop esterno altre 10 volte) e si noterà che mentre è in esecuzione, non è possibile scorrere la pagina. Se funziona abbastanza a lungo, il tuo browser ti chiederà se vuoi terminare il processo, perché non risponde alla pagina. Il frame viene eseguito e il loop degli eventi e la coda dei messaggi rimangono bloccati fino al termine.

Allora perché questo effetto collaterale del testo non si aggiorna? Perché mentre si hanno cambiato il valore dell'elemento nel DOM - è possibile console.log()il suo valore subito dopo averlo cambiato e vedere che è stato modificato (che indica il motivo per cui la spiegazione di DVK non è corretto) - il browser è in attesa per lo stack di impoveriscono (la onfunzione gestore da restituire) e quindi il messaggio da terminare, in modo che possa eventualmente aggirare l'esecuzione del messaggio che è stato aggiunto dal runtime come reazione alla nostra operazione di mutazione e al fine di riflettere quella mutazione nell'interfaccia utente .

Questo perché stiamo effettivamente aspettando che il codice finisca l'esecuzione. Non abbiamo detto "qualcuno lo recuperi e poi chiami questa funzione con i risultati, grazie, e ora ho finito così imma return, fai qualunque cosa ora", come facciamo di solito con il nostro Javascript asincrono basato sugli eventi. Entriamo in una funzione del gestore di eventi click, aggiorniamo un elemento DOM, chiamiamo un'altra funzione, l'altra funzione funziona a lungo e poi ritorna, quindi aggiorniamo lo stesso elemento DOM e poi torniamo dalla funzione iniziale, svuotando efficacemente la pila. E quindi il browser può arrivare al messaggio successivo nella coda, che potrebbe benissimo essere un messaggio generato da noi innescando un evento interno di tipo "mutazione su DOM".

L'interfaccia utente del browser non può (o sceglie di non) aggiornare l'interfaccia utente fino al completamento del frame attualmente in esecuzione (la funzione è tornata). Personalmente, penso che questo sia piuttosto in base alla progettazione che alla restrizione.

Perché la setTimeoutcosa funziona allora? Lo fa, poiché rimuove efficacemente la chiamata alla funzione a esecuzione prolungata dal proprio frame, pianificandone l'esecuzione in un secondo momento nel windowcontesto, in modo che possa tornare immediatamente e consentire alla coda messaggi di elaborare altri messaggi. E l'idea è che il messaggio "on update" dell'interfaccia utente che è stato attivato da noi in Javascript quando si modifica il testo nel DOM è ora in anticipo rispetto al messaggio in coda per la funzione di lunga durata, in modo che l'aggiornamento dell'interfaccia utente avvenga prima del blocco per molto tempo.

Si noti che a) La funzione di lunga durata blocca ancora tutto quando viene eseguita eb) non si è certi che l'aggiornamento dell'interfaccia utente sia effettivamente in anticipo rispetto alla coda dei messaggi. Sul mio browser Chrome di giugno 2018, un valore di 0non "risolve" il problema che il violino dimostra - 10 lo fa. In realtà sono un po 'soffocato da questo, perché mi sembra logico che il messaggio di aggiornamento dell'interfaccia utente debba essere messo in coda prima di esso, poiché il suo trigger viene eseguito prima di pianificare la funzione di esecuzione prolungata per essere eseguita "in seguito". Ma forse ci sono alcune ottimizzazioni nel motore V8 che possono interferire, o forse manca solo la mia comprensione.

Okay, quindi qual è il problema con l'utilizzo setTimeoute qual è la soluzione migliore per questo caso particolare?

Prima di tutto, il problema con l'utilizzo di setTimeoutqualsiasi gestore di eventi come questo, per cercare di alleviare un altro problema, è incline a pasticciare con altro codice. Ecco un esempio di vita reale dal mio lavoro:

Un collega, in una comprensione erroneamente informata sul loop degli eventi, ha provato a "infilare" Javascript facendo uso del codice di rendering del modello setTimeout 0per il suo rendering. Non è più qui per chiedere, ma posso presumere che forse abbia inserito dei timer per misurare la velocità di rendering (che sarebbe l'immediatezza di ritorno delle funzioni) e ho scoperto che l'uso di questo approccio renderebbe le risposte incredibilmente veloci da quella funzione.

Il primo problema è ovvio; non puoi thread javascript, quindi non vinci nulla qui mentre aggiungi offuscamento. In secondo luogo, ora hai effettivamente rimosso il rendering di un modello dalla pila di possibili listener di eventi che potrebbero aspettarsi che quel modello sia stato reso, mentre potrebbe benissimo non esserlo. Il comportamento effettivo di quella funzione era ora non deterministico, come lo era - inconsapevolmente - qualsiasi funzione che l'avrebbe eseguita o dipendesse da essa. Puoi fare congetture istruite, ma non puoi codificare correttamente per il suo comportamento.

La "correzione" durante la scrittura di un nuovo gestore di eventi che dipendeva dalla sua logica era anche quella di utilizzare setTimeout 0. Ma questa non è una correzione, è difficile da capire e non è divertente eseguire il debug di errori causati da codice come questo. A volte non c'è mai nessun problema, altre volte fallisce in modo coerente, e poi di nuovo, a volte funziona e si rompe sporadicamente, a seconda delle prestazioni attuali della piattaforma e di qualsiasi altra cosa accada in quel momento. Questo è il motivo per cui personalmente sconsiglierei di usare questo hack ( è un hack, e dovremmo tutti sapere che lo è), a meno che tu non sappia davvero cosa stai facendo e quali sono le conseguenze.

Ma che cosa possiamo noi fare invece? Bene, come suggerisce l'articolo di MDN di riferimento, o dividere il lavoro in più messaggi (se possibile) in modo che altri messaggi in coda possano essere interfogliati con il lavoro ed eseguiti durante l'esecuzione, oppure utilizzare un lavoratore Web, che può essere eseguito in tandem con la tua pagina e restituisci i risultati quando hai finito con i suoi calcoli.

Oh, e se stai pensando "Beh, non potrei semplicemente mettere un callback nella funzione di lunga durata per renderlo asincrono?", Quindi no. Il callback non lo rende asincrono, dovrà comunque eseguire il codice di lunga durata prima di chiamare esplicitamente il callback.


3

L'altra cosa che fa è spingere l'invocazione della funzione in fondo allo stack, impedendo un overflow dello stack se si chiama in modo ricorsivo una funzione. Ciò ha l'effetto di un whileciclo, ma consente al motore JavaScript di attivare altri timer asincroni.


Downvote per questo push the function invocation to the bottom of the stack. Di cosa stackstai parlando è oscuro. Il più importante è dove setTimeoutaggiunge esattamente questa funzione, la fine di questo ciclo di loop o l'inizio del ciclo di loop successivo.
Green

1

Chiamando setTimeout dai alla pagina il tempo di reagire a qualsiasi cosa l'utente stia facendo. Ciò è particolarmente utile per le funzioni eseguite durante il caricamento della pagina.


1

Alcuni altri casi in cui setTimeout è utile:

Volete interrompere un ciclo o un calcolo a lungo termine in componenti più piccoli in modo che il browser non sembri "bloccarsi" o dire "Lo script nella pagina è occupato".

Si desidera disabilitare un pulsante di invio modulo quando si fa clic, ma se si disabilita il pulsante nel gestore onClick il modulo non verrà inviato. setTimeout con un tempo di zero fa il trucco, permettendo alla fine dell'evento, al modulo di iniziare l'invio, quindi il tuo pulsante può essere disabilitato.


2
La disabilitazione verrebbe eseguita meglio nell'evento onsubmit; sarebbe più veloce ed è garantito che venga chiamato prima che il modulo sia tecnicamente inviato poiché è possibile interrompere l'invio.
Kris

Verissimo. Suppongo che la disabilitazione di onclick sia più facile per la prototipazione perché puoi semplicemente digitare onclick = "this.disabled = true" nel pulsante mentre la disabilitazione su submit richiede un po 'più di lavoro.
fabspro,

1

Le risposte sui cicli di esecuzione e sul rendering del DOM prima del completamento di altri codici sono corrette. Zero secondi timeout in JavaScript aiutano a rendere il codice pseudo-multithread, anche se non lo è.

Voglio aggiungere che il valore MIGLIORE per un timeout zero-second tra browser / multipiattaforma in JavaScript è in realtà di circa 20 millisecondi invece di 0 (zero), perché molti browser mobili non possono registrare timeout inferiori a 20 millisecondi a causa delle limitazioni dell'orologio su chip AMD.

Inoltre, i processi di lunga durata che non comportano la manipolazione del DOM dovrebbero essere inviati ora ai Web Worker, poiché forniscono una vera esecuzione multithread di JavaScript.


2
Sono un po 'scettico sulla tua risposta, ma l'ho votata perché mi ha costretto a fare qualche ricerca aggiuntiva sugli standard del browser. Quando cerco gli standard, vado sempre dove vado, MDN: developer.mozilla.org/en-US/docs/Web/API/window.setTimeout Le specifiche HTML5 dicono 4ms. Non dice nulla sui limiti di clock sui chip mobili. Fare fatica a cercare su Google una fonte di informazioni per il backup delle dichiarazioni. Ho scoperto che Dart Language di Google ha rimosso del tutto setTimeout a favore di un oggetto Timer.
John Zabroski,

8
(...) perché molti browser per dispositivi mobili non possono registrare timeout inferiori a 20 millisecondi a causa dei limiti di clock (...) Ogni piattaforma ha limiti di temporizzazione a causa del suo clock e nessuna piattaforma è in grado di eseguire la cosa successiva esattamente 0 ms dopo il quello attuale. Il timeout di 0 ms richiede l'esecuzione di una funzione il più presto possibile e le limitazioni temporali di una piattaforma specifica non cambiano il significato di ciò in alcun modo.
Piotr Dobrogost,

1

setTimout su 0 è anche molto utile nel modello di impostazione di una promessa differita, che si desidera restituire immediatamente:

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();
}

1

Il problema era che stavi tentando di eseguire un'operazione Javascript su un elemento inesistente. L'elemento doveva ancora essere caricato e setTimeout()offre più tempo per il caricamento di un elemento nei seguenti modi:

  1. setTimeout() causa l'evento asincrono, quindi viene eseguito dopo tutto il codice sincrono, dando al tuo elemento più tempo per il caricamento. I callback asincroni come il callback in setTimeout()vengono inseriti nella coda degli eventi e inseriti nello stack dal loop degli eventi dopo che lo stack di codice sincrono è vuoto.
  2. Il valore 0 per ms come secondo argomento in funzione setTimeout()è spesso leggermente più alto (4-10ms a seconda del browser). Questo tempo leggermente più lungo necessario per eseguire i setTimeout()callback è causato dalla quantità di 'tick' (dove un tick sta spingendo un callback nello stack se lo stack è vuoto) del loop degli eventi. Per motivi di prestazioni e durata della batteria, la quantità di tick nel loop degli eventi è limitata a un determinato importo in meno a 1000 volte al secondo.

-1

Javascript è un'applicazione a thread singolo in modo che non consenta di eseguire la funzione contemporaneamente in modo da ottenere questo utilizzo. Quindi, esattamente ciò che setTimeout (fn, 0) fa che è incastrato nella missione dell'attività che viene eseguita quando lo stack di chiamate è vuoto. So che questa spiegazione è piuttosto noiosa, quindi ti consiglio di leggere questo video che ti aiuterà a capire come funzionano le cose nel browser. Guarda questo video: - https://www.youtube.com/watch?time_continue=392&v=8aGhZQkoFbQ

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.