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 click
all'evento sul #do
pulsante.
Quindi, quando si fa effettivamente clic sul pulsante, message
viene creato un riferimento alla funzione del gestore eventi, che viene aggiunta a message queue
. Quando event loop
raggiunge questo messaggio, viene creato uno frame
stack, 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 on
funzione 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 setTimeout
cosa 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 window
contesto, 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 0
non "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 setTimeout
e qual è la soluzione migliore per questo caso particolare?
Prima di tutto, il problema con l'utilizzo di setTimeout
qualsiasi 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 0
per 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.