“Master the JavaScript Interview” è una serie di post progettati per preparare i candidati a domande comuni che potrebbero incontrare quando fanno domanda per una posizione JavaScript di livello medio-alto. Queste sono domande che uso spesso nelle interviste reali.
Una promessa è un oggetto che potrebbe produrre un singolo valore in futuro: un valore risolto o un motivo per cui non è stato risolto (ad esempio, si è verificato un errore di rete)., Una promessa può essere in uno dei 3 stati possibili: soddisfatta, respinta o in sospeso. Gli utenti Promise possono allegare callback per gestire il valore soddisfatto o il motivo del rifiuto.
Le promesse sono desiderose, il che significa che una promessa inizierà a svolgere qualsiasi compito gli venga assegnato non appena viene invocato il costruttore della promessa. Se hai bisogno di pigro, controlla osservabili o attività.
Una storia incompleta di Promises
Le prime implementazioni di promises and futures (un’idea simile / correlata) cominciarono ad apparire in linguaggi come MultiLisp e Concurrent Prolog già nel 1980., L’uso della parola “promessa” è stato coniato da Barbara Liskov e Liuba Shrira nel 1988.
La prima volta che ho sentito parlare di promesse in JavaScript, Node era nuovo di zecca e la comunità stava discutendo il modo migliore per gestire il comportamento asincrono. La comunità ha sperimentato le promesse per un po’, ma alla fine si è risolta sui callback degli errori standard del nodo.
Nello stesso periodo, Dojo ha aggiunto promesse tramite l’API differita. Il crescente interesse e l’attività alla fine hanno portato alle nuove Promesse/Una specifica progettata per rendere le varie promesse più interoperabili.,
I comportamenti asincroni di jQuery sono stati refactored intorno alle promesse. Il supporto promise di jQuery aveva notevoli somiglianze con il Differito di Dojo, e divenne rapidamente l’implementazione promessa più comunemente usata in JavaScript a causa dell’immensa popolarità di jQuery — per un certo periodo. Tuttavia, non supportava il comportamento di concatenamento a due canali (soddisfatto/rifiutato) & gestione delle eccezioni su cui le persone contavano per creare strumenti in cima alle promesse.,
Nonostante queste debolezze, jQuery ha ufficialmente reso le promesse JavaScript mainstream e le migliori librerie di promesse stand-alone come Q, When e Bluebird sono diventate molto popolari. Le incompatibilità di implementazione di jQuery hanno motivato alcuni importanti chiarimenti nella specifica promise, che è stata riscritta e rinominata come specifica Promises/A+.,
ES6 ha portato a Promises / A + compliantPromise
globale, e alcune API molto importanti sono state costruite in cima al nuovo supporto Promise standard: in particolare la specifica WHATWG Fetch e lo standard Async Functions (una bozza di fase 3 al momento della stesura di questo documento).
Le promesse qui descritte sono quelle compatibili con le specifiche Promises/A+, con particolare attenzione allo standard ECMAScriptPromise
implementazione.
Come funzionano le promesse
Una promessa è un oggetto che può essere restituito in modo sincrono da una funzione asincrona., Sarà in uno dei 3 stati possibili:
Una promessa viene risolta se non è in sospeso (è stata risolta o rifiutata). A volte le persone usano risolte e risolte per significare la stessa cosa: non in sospeso.
Una volta risolta, una promessa non può essere reinsediata. Chiamare nuovamenteresolve()
oreject()
non avrà alcun effetto. L’immutabilità di una promessa stabilita è una caratteristica importante.
Le promesse JavaScript native non espongono gli stati delle promesse. Invece, ci si aspetta che tratti la promessa come una scatola nera., Solo la funzione responsabile della creazione della promessa avrà conoscenza dello stato della promessa o accesso per risolvere o rifiutare.
Qui è una funzione che restituisce una promessa che si risolve dopo un determinato tempo di ritardo:
il wait(3000)
chiamata in attesa 3000ms (3 secondi), e poi log 'Hello!'
., Tutte le promesse compatibili con le specifiche definiscono un metodo.then()
che si utilizza per passare i gestori che possono assumere il valore risolto o rifiutato.
Il costruttore ES6 promise accetta una funzione. Questa funzione accetta due parametri,resolve()
ereject()
. Nell’esempio sopra, stiamo usando solo resolve()
, quindi ho lasciato reject()
fuori dall’elenco dei parametri. Quindi chiamiamo setTimeout()
per creare il ritardo e chiamiamoresolve()
quando è finito.,
È possibile opzionalmenteresolve()
oreject()
con valori, che verranno passati alle funzioni di callback associate con.then()
.
Quando I reject()
con un valore, passo sempre un Error
oggetto. Generalmente voglio due possibili stati di risoluzione: il normale percorso felice o un’eccezione — tutto ciò che impedisce al normale percorso felice di accadere. Il passaggio di un oggettoError
lo rende esplicito.,
Regole importanti delle promesse
Uno standard per le promesse è stato definito dalla comunità delle specifiche Promises / A+. Ci sono molte implementazioni conformi allo standard, incluse le promesse ECMAScript standard JavaScript.
Le promesse che seguono le specifiche devono seguire un insieme specifico di regole:
- Una promessa o “thenable” è un oggetto che fornisce un metodo
.then()
conforme allo standard. - Una promessa in sospeso può passare a uno stato soddisfatto o rifiutato.,
- Una promessa soddisfatta o rifiutata è risolta e non deve passare a nessun altro stato.
- Una volta che una promessa è risolta, deve avere un valore (che può essere
undefined
). Questo valore non deve cambiare.
Il cambiamento in questo contesto si riferisce al confronto identity (===
). Un oggetto può essere utilizzato come valore soddisfatto e le proprietà dell’oggetto possono mutare.,
Ogni promessa, deve fornire un .then()
metodo con la seguente firma:
promise.then(
onFulfilled?: Function,
onRejected?: Function
) => Promise
.then()
metodo deve rispettare queste regole:
- Sia
onFulfilled()
eonRejected()
sono opzionali. - Se gli argomenti forniti non sono funzioni, devono essere ignorati.
-
onFulfilled()
verrà chiamato dopo che la promessa è stata soddisfatta, con il valore della promessa come primo argomento., -
onRejected()
verrà chiamato dopo che la promessa è stata rifiutata, con il motivo del rifiuto come primo argomento. Il motivo potrebbe essere qualsiasi valore JavaScript valido, ma poiché i rifiuti sono essenzialmente sinonimi di eccezioni, consiglio di utilizzare gli oggetti Error. - Né
onFulfilled()
néonRejected()
possono essere chiamati più di una volta. -
.then()
può essere chiamato molte volte sulla stessa promessa. In altre parole, una promessa può essere utilizzata per aggregare i callback., -
.then()
deve restituire una nuova promessa,promise2
. - Se
onFulfilled()
oonRejected()
restituire un valorex
ex
è una promessapromise2
bloccare con (si assume che lo stesso stato e valore)x
. In caso contrario,promise2
verrà soddisfatto con il valore dix
., - Se
onFulfilled
oonRejected
genera un’eccezionee
promise2
deve essere respinto cone
come la ragione. - Se
onFulfilled
non è una funzione epromise1
è soddisfatta,promise2
deve essere soddisfatta con lo stesso valore dipromise1
., - Se
onRejected
non è una funzione epromise1
viene rifiutato,promise2
deve essere rifiutato con lo stesso motivo dipromise1
.
Promise Chaining
Poiché.then()
restituisce sempre una nuova promessa, è possibile concatenare le promesse con un controllo preciso su come e dove vengono gestiti gli errori. Le promesse consentono di imitare il normale codice sincrono try
/catch
comportamento.,
Come il codice sincrono, il concatenamento si tradurrà in una sequenza che viene eseguita in seriale., In altre parole, si può fare:
fetch(url)
.then(process)
.then(save)
.catch(handleErrors)
;
Ecco un esempio di un complesso promessa catena con più rifiuti:
Errore di Gestione
Nota che promette di avere sia un successo e un gestore di errori, ed è molto comune vedere il codice che fa questo:
save().then(
handleSuccess,
handleError
);
Ma cosa succede se handleSuccess()
genera un errore?, La promessa restituita da .then()
verrà rifiutata, ma non c’è nulla lì per catturare il rifiuto, il che significa che un errore nella tua app viene inghiottito. Ops!
Per questo motivo, alcune persone considerano il codice sopra un anti-pattern e raccomandano invece quanto segue:
save()
.then(handleSuccess)
.catch(handleError)
;
La differenza è sottile, ma importante. Nel primo esempio, verrà rilevato un errore originato dall’operazione save()
, ma verrà rilevato un errore originato dalla funzione handleSuccess()
.,
In the second example, .catch()
will handle rejections from either save()
, or handleSuccess()
.
Naturalmente, l’erroresave()
potrebbe essere un errore di rete, mentre l’errorehandleSuccess()
potrebbe essere dovuto al fatto che lo sviluppatore ha dimenticato di gestire un codice di stato specifico. Cosa succede se si desidera gestirli in modo diverso? Puoi scegliere di gestirli entrambi:
save()
.then(
handleSuccess,
handleNetworkError
)
.catch(handleProgrammerError)
;
Qualunque cosa tu preferisca, ti consiglio di terminare tutte le catene di promesse con un .catch()
. Vale la pena ripetere:
Consiglio di terminare tutte le catene di promesse con un
.catch()
.,
Come posso annullare una promessa?
Una delle prime cose che gli utenti di nuove promesse spesso si chiedono è come annullare una promessa. Ecco un’idea: basta rifiutare la promessa con “Annullato” come motivo. Se è necessario gestirlo in modo diverso rispetto a un errore “normale”, eseguire la ramificazione nel gestore degli errori.
Ecco alcuni errori comuni che le persone commettono quando annullano la propria promessa:
Aggiunta .,cancel () alla promessa
L’aggiunta di.cancel()
rende la promessa non standard, ma viola anche un’altra regola delle promesse: solo la funzione che crea la promessa dovrebbe essere in grado di risolvere, rifiutare o annullare la promessa. Esponendolo rompe quell’incapsulamento e incoraggia le persone a scrivere codice che manipola la promessa in luoghi che non dovrebbero saperlo. Evita gli spaghetti e le promesse non mantenute.
Dimenticando di ripulire
Alcune persone intelligenti hanno capito che c’è un modo per usarePromise.race()
come meccanismo di cancellazione., Il problema è che il controllo di cancellazione viene preso dalla funzione che crea la promessa, che è l’unico posto in cui è possibile condurre attività di pulizia appropriate, come cancellare i timeout o liberare memoria cancellando i riferimenti ai dati, ecc…
Dimenticando di gestire una promessa di annullamento rifiutata
Lo sapevate che Chrome lancia messaggi di avviso su tutta la console quando si dimentica di gestire un rifiuto della promessa? Ops!
Eccessivamente complesso
La proposta di cancellazione TC39 ritirata proponeva un canale di messaggistica separato per le cancellazioni., Ha anche utilizzato un nuovo concetto chiamato token di cancellazione. A mio parere, la soluzione avrebbe notevolmente gonfiato le specifiche promise, e l’unica caratteristica che avrebbe fornito che le speculazioni non supportano direttamente è la separazione dei rifiuti e delle cancellazioni, che, IMO, non è necessario per cominciare.
Vuoi cambiare a seconda che ci sia un’eccezione o una cancellazione? Si’, assolutamente. E ‘ compito della promise? Secondo me, no, non lo è.,
Ripensare la cancellazione della promessa
Generalmente, passo tutte le informazioni necessarie alla promessa per determinare come risolvere / rifiutare / annullare al momento della creazione della promessa. In questo modo, non è necessario un metodo .cancel()
su una promessa. Ci si potrebbe chiedere come si potrebbe sapere se si sta andando ad annullare al momento della creazione promessa.
“Se non so ancora se annullare o meno, come faccio a sapere cosa passare quando creo la promessa?,”
Se solo ci fosse un qualche tipo di oggetto che potrebbe sostituire un valore potenziale in futuro oh oh, aspetta.
Il valore che passiamo per rappresentare se annullare o meno potrebbe essere una promessa stessa. Ecco come potrebbe apparire:
Stiamo usando l’assegnazione dei parametri predefiniti per dirgli di non annullare per impostazione predefinita. Ciò rende il parametrocancel
comodamente opzionale., Quindi impostiamo il timeout come abbiamo fatto prima, ma questa volta catturiamo l’ID del timeout in modo da poterlo cancellare in un secondo momento.
Utilizziamo il metodocancel.then()
per gestire la cancellazione e la pulizia delle risorse. Questo verrà eseguito solo se la promessa viene annullata prima che abbia la possibilità di risolvere. Se si annulla troppo tardi, hai perso la tua occasione. Quel treno ha lasciato la stazione.
Nota: Ci si potrebbe chiedere a cosa serve la funzione
noop()
. La parola noop sta per no-op, che significa una funzione che non fa nulla., Senza di esso, V8 genererà avvisi:UnhandledPromiseRejectionWarning: Unhandled promise rejection
. È una buona idea gestire sempre i rifiuti promise, anche se il tuo gestore è unnoop()
.
Abstracting Promise Cancellation
Questo va bene per un timerwait()
, ma possiamo astrarre ulteriormente questa idea per incapsulare tutto ciò che devi ricordare:
- Rifiuta la promessa di annullamento per impostazione predefinita — non vogliamo annullare o generare errori se non viene passata alcuna promessa di annullamento.
- Ricordarsi di eseguire la pulizia quando si rifiuta per le cancellazioni.,
- Ricorda che la pulizia
onCancel
potrebbe generare un errore, e anche quell’errore dovrà essere gestito. (Si noti che la gestione degli errori è omessa nell’esempio di attesa sopra-è facile da dimenticare!)
Creiamo un programma di utilità di promessa cancellabile che è possibile utilizzare per avvolgere qualsiasi promessa., Per esempio, per gestire le richieste di rete, ecc… La firma sarà simile a questa:
speculation(fn: SpecFunction, shouldCancel: Promise) => Promise
Il SpecFunction è proprio come la funzione di passare nel Promise
costruttore, con una sola eccezione — ci vuole un onCancel()
gestore:
SpecFunction(resolve: Function, reject: Function, onCancel: Function) => Void
si noti che in questo esempio è solo un esempio per dare il senso di come funziona. Ci sono alcuni altri casi limite che devi prendere in considerazione., Ad esempio, in questa versione, handleCancel
verrà chiamato se si annulla la promessa dopo che è già stata risolta.
Ho implementato una versione di produzione mantenuta di questo con casi limite coperti come libreria open source, Speculazione.
Usiamo l’astrazione della libreria migliorata per riscrivere l’utilità cancellabilewait()
da prima., Prima di installare la speculazione:
npm install --save speculation
Ora è possibile importare e utilizzare:
Questo semplifica un po ‘ le cose, perché non avete bisogno di preoccuparsi per la noop()
, la cattura di errori nel onCancel()
funzione, o in altri casi di bordo. Questi dettagli sono stati astratti da speculation()
. Check it out e sentitevi liberi di usarlo in progetti reali.,
Extra della promessa JS nativa
L’oggetto nativoPromise
ha alcune cose extra che potrebbero interessarti:
-
Promise.reject()
restituisce una promessa rifiutata. -
Promise.resolve()
restituisce una promessa risolta. -
Promise.race()
accetta un array (o qualsiasi iterabile) e restituisce una promessa che si risolve con il valore della prima promessa risolta nell’iterabile, o rifiuta con il motivo della prima promessa che rifiuta., -
Promise.all()
prende un array (o qualsiasi iterabile) e restituisce una promessa che si risolve quando tutte le promesse nell’argomento iterabile sono state risolte, o rifiuta con il motivo della prima promessa passata che rifiuta.
Conclusione
Le promesse sono diventate parte integrante di diversi idiomi in JavaScript, incluso lo standard WHATWG Fetch utilizzato per la maggior parte delle richieste ajax moderne e lo standard Async Functions utilizzato per rendere sincrono il codice asincrono.,
Le funzioni asincrone sono allo stadio 3 al momento della stesura di questo articolo, ma prevedo che diventeranno presto una soluzione molto popolare e molto comunemente usata per la programmazione asincrona in JavaScript — il che significa che imparare ad apprezzare le promesse sarà ancora più importante per gli sviluppatori JavaScript nel prossimo futuro.
Ad esempio, se stai usando Redux, ti suggerisco di controllare redux-saga: una libreria utilizzata per gestire gli effetti collaterali in Redux che dipende dalle funzioni asincrone in tutta la documentazione.,
Spero che anche gli utenti esperti di promise abbiano una migliore comprensione di quali promesse sono e come funzionano, e come usarle meglio dopo aver letto questo.
Esplora la serie
- Che cos’è una chiusura?
- Qual è la differenza tra l’ereditarietà di classe e prototipale?
- Che cos’è una funzione pura?
- Che cos’è la composizione della funzione?
- Che cos’è la programmazione funzionale?
- Che cos’è una promessa?
- Competenze trasversali