2016-04-26 33 views
19

Mi piacerebbe spiegare a me stesso l'ordine di esecuzione del seguente snippet che utilizza le promesse di javascript.Qual è l'ordine di esecuzione in javascript promesse

Promise.resolve('A') 
    .then(function(a){console.log(2, a); return 'B';}) 
    .then(function(a){ 
    Promise.resolve('C') 
     .then(function(a){console.log(7, a);}) 
     .then(function(a){console.log(8, a);}); 
    console.log(3, a); 
    return a;}) 
    .then(function(a){ 
    Promise.resolve('D') 
     .then(function(a){console.log(9, a);}) 
     .then(function(a){console.log(10, a);}); 
    console.log(4, a);}) 
    .then(function(a){ 
    console.log(5, a);}); 
console.log(1); 
setTimeout(function(){console.log(6)},0); 

Il risultato è:

1 
2 "A" 
3 "B" 
7 "C" 
4 "B" 
8 undefined 
9 "D" 
5 undefined 
10 undefined 
6 

Sono curioso di sapere l'ordine di esecuzione 1 2 3 7 ... non i valori di 'A', 'B' ...

La mia comprensione è che se una promessa viene risolta, la funzione "then" viene inserita nella coda degli eventi del browser. Così la mia aspettativa era 1 2 3 4 ...


@ jfriend00 Grazie, grazie mille per le spiegazioni dettagliate! È davvero un'enorme quantità di lavoro!

+1

Le promesse funzionano con il valore 'return' non sono magiche :) Se non si restituisce da un' then', non funzionerà. Se aggiungi 'return' per aggiungere le tue funzioni, funzionerà come previsto. Ci sono come 100 duplicati di questo - aspetterò solo che Bergi o fidanzato ne facciano notare uno buono. –

+0

* "Sono curioso sull'ordine di esecuzione 1 2 3 7 ... non sui valori 'A', 'B' ..." * Quindi rimuovi le informazioni e il codice estranei dalla domanda. –

+1

se non si restituisce esplicitamente 'return' dalla promessa, esso restituirà 'undefined' implicitamente – Srle

risposta

42

Commenti

Prima di tutto, in esecuzione promesse all'interno di un gestore di .then() e non restituendo quelle promesse dal .then() callback crea una nuova sequenza di promessa senza legami che non è sincronizzato con le promesse madri in alcun modo. Di solito, questo è un bug e, in effetti, alcuni motori di promessa avvertono effettivamente quando lo fai perché non è quasi mai il comportamento desiderato. L'unica volta che uno vorrebbe mai farlo è quando stai facendo una specie di incendio e dimentichi un'operazione in cui non ti importa degli errori e non ti interessa sincronizzare con il resto del mondo.

Quindi, tutte le tue promesse Promise.resolve() all'interno dei gestori di .then() creano nuove catene Promise eseguite indipendentemente dalla catena principale. Non hai un comportamento determinato. È un po 'come lanciare quattro chiamate Ajax in parallelo. Non sai quale sarà il primo ad essere completato. Ora, dato che tutto il codice all'interno di quei gestori Promise.resolve() è sincrono (poiché non si tratta di codice reale), si potrebbe ottenere un comportamento coerente, ma non è il punto di progettazione delle promesse, quindi non spenderei molto tempo cercando di capire quale catena di Promise che esegue solo il codice sincrono finirà per prima. Nel mondo reale, non importa perché se l'ordine conta, allora non lascerai le cose al caso in questo modo.

Sommario

  1. Tutti .then() gestori vengono chiamati in modo asincrono dopo il thread corrente di finiture di esecuzione (come le promesse/A + spec dice, quando il motore JS ritorna al "codice della piattaforma"). Questo è vero anche per le promesse che vengono risolte in modo sincrono come Promise.resolve().then(...).Ciò avviene per coerenza di programmazione in modo che un gestore .then() venga chiamato in modo coerente in modo asincrono, indipendentemente dal fatto che la promessa sia risolta immediatamente o successivamente. Ciò impedisce alcuni bug di temporizzazione e rende più semplice per il codice chiamante vedere l'esecuzione asincrona coerente.

  2. Non esiste alcuna specifica che determina l'ordine relativo di setTimeout() rispetto ai gestori di linea .then() se entrambi sono in coda e pronti per l'esecuzione. Nell'implementazione, un gestore .then() in attesa viene sempre eseguito prima di uno setTimeout() in sospeso, ma le specifiche di specifica Promises/A + affermano che questo non è determinato. Si dice che i gestori di .then() possono essere pianificati in molti modi, alcuni dei quali verrebbero eseguiti prima delle chiamate in attesa setTimeout() e alcuni dei quali potrebbero essere eseguiti dopo le chiamate in attesa setTimeout(). Ad esempio, la specifica Promises/A + consente di pianificare i gestori .then() con setImmediate(), che dovrebbe essere eseguito prima delle chiamate setTimeout() in sospeso o con setTimeout() che verrebbero eseguiti dopo le chiamate in attesa setTimeout(). Quindi, il tuo codice non dovrebbe dipendere da quell'ordine.

  3. Più catene di promessa indipendenti non hanno un ordine prevedibile di esecuzione e non è possibile fare affidamento su alcun ordine particolare. È come sparare quattro chiamate ajax in parallelo dove non sai quale sarà il primo ad essere completato.

  4. Se l'ordine di esecuzione è importante, non creare una gara che dipenda da dettagli di implementazione minuti. Invece, collegare le catene di promessa per forzare un particolare ordine di esecuzione.

  5. Generalmente non si desidera creare catene di promesse indipendenti all'interno di un gestore .then() che non vengono restituite dal gestore. Questo di solito è un errore tranne che in rari casi di incendio e si dimentica senza la gestione degli errori.

riga per riga Analsysis

Quindi, ecco l'analisi del codice. Ho aggiunto i numeri di riga e ripulito il rientro per rendere più facile da discutere:

1  Promise.resolve('A').then(function (a) { 
2   console.log(2, a); 
3   return 'B'; 
4  }).then(function (a) { 
5   Promise.resolve('C').then(function (a) { 
6    console.log(7, a); 
7   }).then(function (a) { 
8    console.log(8, a); 
9   }); 
10  console.log(3, a); 
11  return a; 
12 }).then(function (a) { 
13  Promise.resolve('D').then(function (a) { 
14   console.log(9, a); 
15  }).then(function (a) { 
16   console.log(10, a); 
17  }); 
18  console.log(4, a); 
19 }).then(function (a) { 
20  console.log(5, a); 
21 }); 
22 
23 console.log(1); 
24  
25 setTimeout(function() { 
26  console.log(6) 
27 }, 0); 

Linea 1 inizia una catena promessa e attaccato un gestore .then() ad esso. Poiché Promise.resolve() si risolve immediatamente, la libreria Promise pianificherà il primo gestore .then() da eseguire dopo il termine di questo thread di Javascript. Nelle librerie promesse/promesse compatibili A +, tutti i gestori .then() vengono chiamati in modo asincrono dopo il termine del thread di esecuzione corrente e quando JS torna al ciclo degli eventi. Ciò significa che verrà eseguito qualsiasi altro codice sincrono in questo thread, ad esempio il tuo console.log(1), che è quello che vedi.

Tutti gli altri .then() gestori al livello superiore (linee 4, 12, 19) catena dopo il primo e verrà eseguita solo dopo il primo prende il turno. Sono essenzialmente in coda a questo punto.

Poiché lo setTimeout() si trova anche in questo thread iniziale di esecuzione, viene eseguito e pertanto è programmato un timer.

Questa è la fine dell'esecuzione sincrona. Ora, il motore JS inizia a eseguire gli eventi pianificati nella coda degli eventi.

Per quanto ne so, non vi è alcuna garanzia che venga prima un gestore setTimeout(fn, 0) o un gestore .then() entrambi pianificati per l'esecuzione subito dopo questo thread di esecuzione. I gestori di .then() sono considerati "micro-task", quindi non mi sorprende che vengano eseguiti prima dello setTimeout(). Ma, se hai bisogno di un ordine particolare, dovresti scrivere un codice che garantisca un ordine piuttosto che fare affidamento su questi dettagli di implementazione.

Ad ogni modo, il gestore .then() definito su riga 1 viene eseguito in seguito. Così vedi l'output 2 "A" da quello console.log(2, a).

Avanti, dal momento che il precedente gestore .then() restituito un valore normale, quella promessa è considerata risolta in modo che il gestore .then() definito sulla linea 4 piste. Ecco dove stai creando un'altra catena di promesse indipendente e introducendo un comportamento che di solito è un bug.

Riga 5, crea una nuova catena Promise. Risolve quella promessa iniziale e quindi pianifica due gestori .then() da eseguire quando viene eseguito il thread di esecuzione corrente. Nell'attuale thread di esecuzione c'è il console.log(3, a) sulla riga 10, ecco perché lo vedrai dopo. Quindi, questo thread di esecuzione termina e torna allo scheduler per vedere cosa eseguire successivamente.

Ora abbiamo diversi gestori .then() in coda in attesa di eseguire il prossimo. C'è quella che abbiamo appena programmato sulla linea 5 e c'è quella successiva nella catena di più alto livello sulla linea 12. Se aveste fatto questo su linea 5:

return Promise.resolve.then(...) 

allora si sarebbe collegato queste promesse insieme e sarebbero coordinati in sequenza. Ma, non restituendo il valore promettente, hai iniziato una nuova intera catena di promesse che non è coordinata con la promessa esteriore di livello superiore. Nel tuo caso particolare, lo schedulatore promette di eseguire successivamente il gestore .then() più profondamente annidato. Non so onestamente se ciò avvenga per specifica, per convenzione o solo per un dettaglio di implementazione di un motore di promessa rispetto all'altro. Direi che se l'ordine è fondamentale per te, allora dovresti forzare un ordine collegando le promesse in un ordine specifico piuttosto che fare affidamento su chi vince la corsa per correre prima.

In ogni caso, nel tuo caso, si tratta di una gara di programmazione e il motore si esegue decide di eseguire l'interno .then() gestore che è definito sulla linea 5 successiva e quindi si vede il 7 "C" prescritta sulla linea 6. Quindi non restituisce nulla, quindi il valore risolto di questa promessa diventa undefined.

Indietro nello scheduler, esegue il gestore .then() su riga 12. Questa è di nuovo una gara tra il gestore .then() e quello su linea 7 che è anche in attesa di essere eseguito. Non so perché si scelga uno sull'altro qui oltre a dire che potrebbe essere indeterminato o variare per motore di promessa perché l'ordine non è specificato dal codice. In ogni caso, il gestore .then() nella riga inizia a funzionare. Ciò crea nuovamente una nuova catena di catene di promesse indipendenti o non sincronizzate rispetto alla precedente. Pianifica di nuovo un gestore .then() e quindi si ottiene il 4 "B" dal codice sincrono nel gestore .then(). Tutto il codice sincrono è fatto in quel gestore così ora, torna allo scheduler per l'attività successiva.

Back in the scheduler, si decide di eseguire il gestore .then() on linea 7 e si ottiene 8 undefined. La promessa è undefined perché il precedente gestore .then() in quella catena non ha restituito nulla, quindi il suo valore di ritorno era undefined, quindi questo è il valore risolto della catena di promessa in quel punto.

A questo punto, l'uscita finora è:

1 
2 "A" 
3 "B" 
7 "C" 
4 "B" 
8 undefined 

Ancora, tutto il codice sincrono è fatto in modo che risale al programmatore di nuovo e decide di eseguire il gestore .then() definita su linea 13 . Funziona e ottieni l'output 9 "D" e poi torna di nuovo allo scheduler.

Coerentemente con il già annidato Promise.resolve() catena, la pianificazione sceglie di eseguire il successivo gestore .then() esterna definita su linea 19. Funziona e ottieni l'output 5 undefined. È ancora undefined poiché il precedente gestore .then() in quella catena non ha restituito un valore, pertanto il valore risolto della promessa era undefined.

Come questo punto, l'uscita finora è:

1 
2 "A" 
3 "B" 
7 "C" 
4 "B" 
8 undefined 
9 "D" 
5 undefined 

A questo punto, c'è solo una .then() handler programmato per essere eseguito in modo che sia quello definito sulla linea 15 e si ottiene il uscita 10 undefined avanti.

Poi, infine, la setTimeout() arriva a correre e il risultato finale è:

1 
2 "A" 
3 "B" 
7 "C" 
4 "B" 
8 undefined 
9 "D" 
5 undefined 
10 undefined 
6 

Se si dovesse cercare di prevedere esattamente l'ordine di ciò sarebbe, poi ci sarebbero due questioni principali.

  1. Come sono in attesa .then() gestori priorità vs. setTimeout() chiamate che vengono anche in attesa.

  2. In che modo il motore delle promesse decide di dare la priorità a più gestori .then() che sono tutti in attesa di essere eseguiti. Per i tuoi risultati con questo codice non è FIFO.

Per la prima domanda, non so se questo è secondo la specifica o solo una scelta di attuazione qui nel motore del motore promessa/JS, ma l'implementazione è stata comunicata sul sembra privilegiare tutte pendenti .then() gestori di prima qualsiasi chiamata setTimeout(). Il tuo caso è un po 'strano perché non hai chiamate API asincrone effettive oltre a specificare i gestori .then(). Se avevi qualche operazione asincrona che in realtà richiedeva tempo reale per l'esecuzione all'inizio di questa catena di promesse, il tuo setTimeout() veniva eseguito prima del gestore .then() sulla vera operazione asincrona solo perché l'operazione asincrona reale richiede tempo effettivo per l'esecuzione. Quindi, questo è un esempio un po 'forzato e non è il solito caso di design per il codice reale.

Per la seconda domanda, ho visto alcune discussioni che illustrano come dare priorità ai gestori .then() in attesa di livelli diversi di nidificazione. Non so se quella discussione sia mai stata risolta in una specifica o no. Preferisco codificare in modo tale che quel livello di dettaglio non mi importi. Se mi interessa l'ordine delle mie operazioni asincrone, collego le catene delle mie promesse per controllare l'ordine e questo livello di dettaglio dell'implementazione non mi riguarda in alcun modo. Se non mi interessa l'ordine, allora non mi interessa l'ordine, quindi ancora quel livello di dettaglio dell'implementazione non mi riguarda. Anche se questo era in alcune specifiche, sembra il tipo di dettaglio che non dovrebbe essere considerato attendibile attraverso molte diverse implementazioni (diversi browser, diversi motori di promessa) a meno che non lo aveste testato ovunque steste andando a correre. Quindi, ti consiglio di non fare affidamento su uno specifico ordine di esecuzione quando hai catene di promesse non sincronizzate.


Si potrebbe fare l'% determinato ordine 100, semplicemente collegando tutte le vostre catene promessa come questo (il ritorno promesse interiori in modo che siano collegate in catena genitore):

Promise.resolve('A').then(function (a) { 
    console.log(2, a); 
    return 'B'; 
}).then(function (a) { 
    var p = Promise.resolve('C').then(function (a) { 
     console.log(7, a); 
    }).then(function (a) { 
     console.log(8, a); 
    }); 
    console.log(3, a); 
    // return this promise to chain to the parent promise 
    return p; 
}).then(function (a) { 
    var p = Promise.resolve('D').then(function (a) { 
     console.log(9, a); 
    }).then(function (a) { 
     console.log(10, a); 
    }); 
    console.log(4, a); 
    // return this promise to chain to the parent promise 
    return p; 
}).then(function (a) { 
    console.log(5, a); 
}); 

console.log(1); 

setTimeout(function() { 
    console.log(6) 
}, 0); 

Questo ha pronunciato la seguente uscita in Chrome:

1 
2 "A" 
3 "B" 
7 "C" 
8 undefined 
4 undefined 
9 "D" 
10 undefined 
5 undefined 
6 

E, dal momento che la promessa sono stati tutti incatenato insieme, l'ordine promessa è tutto definito dal codice. L'unica cosa rimasta come dettaglio di implementazione è la tempistica dello setTimeout() che, come nel tuo esempio, arriva per ultimo, dopo tutti i gestori in sospeso .then().

Edit:

Dopo un esame del Promises/A+ specification, troviamo questo:

2.2.4 onFulfilled o onRejected non deve essere chiamato fino a quando lo stack di contesto di esecuzione contiene soltanto il codice della piattaforma. [3.1].

....

3.1 Qui “codice della piattaforma” significa il motore, l'ambiente, e codice di implementazione promessa. In pratica, questo requisito garantisce che onFulfilled e onRejected vengano eseguiti in modo asincrono, dopo il ciclo di eventi in cui viene quindi chiamato e con uno stack nuovo. Questo può essere implementato con un meccanismo "macro-task" come setTimeout o setImmediate o con un meccanismo "micro-task" come MutationObserver o process.nextTick. Poiché l'implementazione promessa è considerata codice di piattaforma, può contenere una coda di pianificazione attività o "trampolino" in cui vengono chiamati i gestori.

Questo dice che .then() gestori devono eseguire in modo asincrono dopo gli stack di chiamate torna al codice della piattaforma, ma lascia interamente alla realizzazione come esattamente per farlo se è fatto con un macro-task, come setTimeout() o micro-task come process.nextTick(). Quindi, secondo questa specifica, non è determinato e non dovrebbe essere invocato.

Non trovo informazioni sulle macro-attività, sulle micro-attività o sui tempi delle promesse .then() gestori in relazione a setTimeout() nella specifica ES6.Questo forse non è sorprendente dato che lo stesso setTimeout() non fa parte delle specifiche ES6 (è una funzione di ambiente host, non una funzione di linguaggio).

Non ho trovato alcuna specifica per eseguire il backup, ma le risposte a questa domanda Difference between microtask and macrotask within an event loop context spiegano come le cose tendono a funzionare nei browser con macro-attività e micro-attività.

FYI, se desideri maggiori informazioni su micro-attività e macro-attività, ecco un interessante articolo di riferimento sull'argomento: Tasks, microtasks, queues and schedules.

+0

Aggiunte informazioni sul tempo di attesa dei gestori '.then()' rispetto a 'setTimeout()' dalla specifica [Promises/A +] (https://promisesaplus.com/). – jfriend00

+1

Perché il downvote? Ho messo un'enorme quantità di lavoro per spiegare come funziona tutto questo. Almeno si potrebbe fare è spiegare perché pensi che questo meriti un downvote. Se qualcosa è impreciso e puoi indicare cosa, sarò felice di correggerlo. – jfriend00

+0

"Per quanto ne so, non c'è alcuna garanzia che viene prima ...", dipende interamente dall'implementazione delle promesse. 'setTimeout' ha effettivamente un ritardo di timeout minimo (qualcosa come IIRC' 15ms') ma Promises * può * usare 'setImmediate', che verrebbe naturalmente aggiunto al ciclo di eventi JS prima di un timer impostato allo stesso tempo. Le implementazioni promesse spesso usano 'setTimeout (fn, 0)', che poi si risolvono nell'ordine in cui scadono i loro timer minimi, cosa che accadrebbe nell'ordine in cui sono chiamati. – zzzzBov

1

Il motore JavaScript del browser ha qualcosa chiamato "loop eventi". C'è solo un thread di codice JavaScript in esecuzione alla volta. Quando si fa clic su un pulsante o su una richiesta AJAX o qualsiasi altra operazione asincrona, viene inserito un nuovo evento nel ciclo degli eventi. Il browser esegue questi eventi uno alla volta.

Quello che stai guardando qui è che si esegue codice che viene eseguito in modo asincrono. Al termine del codice asincrono, aggiunge un evento appropriato al ciclo degli eventi. L'ordine in cui gli eventi vengono aggiunti dipende dalla durata di ciascuna operazione asincrona da completare.

Ciò significa che se si utilizza qualcosa come AJAX in cui non si ha il controllo sull'ordine in cui verranno completate le richieste, le promesse possono essere eseguite in un ordine diverso ogni volta.

+1

In realtà, nella maggior parte dei browser, i callback' .then' vengono eseguiti su una coda microtask che viene svuotata alla fine dell'attuale run-to- completamento, prima che il ciclo dell'evento principale giri. – jib