Padroneggia l'intervista a JavaScript: cos'è una promessa?

Foto di Kabun (CC BY NC SA 2.0)
"Padroneggia l'intervista a JavaScript" è una serie di post progettati per preparare i candidati alle domande più comuni che possono incontrare quando fanno domanda per una posizione JavaScript di livello medio-alto. Queste sono domande che utilizzo frequentemente in interviste reali.

Che cos'è una promessa?

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: adempiuto, rifiutato o in sospeso. Gli utenti promettenti possono allegare callback per gestire il valore soddisfatto o il motivo del rifiuto.

Le promesse sono impazienti, il che significa che una promessa inizierà a fare qualsiasi compito le dai non appena viene invocato il costruttore della promessa. Se hai bisogno di un pigro, controlla osservabili o attività.

Una storia incompleta di promesse

Le prime implementazioni di promesse e futuri (un'idea simile / correlata) iniziarono ad apparire in lingue come MultiLisp e Concolog Prolog già dagli anni '80. L'uso della parola "promessa" è stato coniato da Barbara Liskov e Liuba Shrira nel 1988 [1].

La prima volta che ho sentito delle promesse in JavaScript, Node era nuovo di zecca e la community stava discutendo il modo migliore per gestire il comportamento asincrono. La comunità ha sperimentato le promesse per un po ', ma alla fine ha optato per i callback prima di errori standard del nodo.

Allo stesso tempo, Dojo ha aggiunto promesse tramite l'API differita. L'interesse e l'attività crescenti alla fine portarono alle nuove promesse / Una specifica progettata per rendere più interoperabili le varie promesse.

I comportamenti asincroni di jQuery sono stati riformulati attorno alle promesse. Il supporto alle promesse di jQuery aveva notevoli somiglianze con il rinviato di Dojo, e divenne rapidamente l'implementazione delle promesse più comunemente usata in JavaScript a causa dell'immensa popolarità di jQuery - per un certo periodo. Tuttavia, non supportava il comportamento di concatenamento e gestione delle eccezioni a due canali (adempiuto / rifiutato) su cui le persone contavano per costruire strumenti in linea con le promesse.

Nonostante questi punti deboli, jQuery ha reso ufficialmente le promesse di JavaScript più diffuse e le librerie di promesse indipendenti come Q, When e Bluebird sono diventate molto popolari. Le incompatibilità di implementazione di jQuery hanno motivato alcuni importanti chiarimenti nelle specifiche promesse, che sono state riscritte e rinominate come specifiche Promesse / A +.

ES6 ha portato un Promise / A + conforme a Promise globale e alcune API molto importanti sono state costruite in aggiunta al nuovo supporto Promise standard: in particolare le specifiche WHATWG Fetch e lo standard Async Functions (una bozza della fase 3 al momento della stesura di questo scritto).

Le promesse qui descritte sono compatibili con la specifica Promises / A +, con particolare attenzione all'implementazione Promise dello standard ECMAScript.

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:

  • Adempiuto: verrà chiamato onFulfilled () (ad esempio, è stato chiamato resolve ())
  • Rifiutato: verrà chiamato onRejected () (ad esempio, è stato chiamato rifiutare ())
  • In sospeso: non ancora soddisfatto o rifiutato

Una promessa viene stabilita se non è in sospeso (è stata risolta o respinta). A volte le persone usano risolto e risolto per significare la stessa cosa: non in sospeso.

Una volta stabilita, una promessa non può essere reinsediata. La chiamata di nuova risoluzione () o rifiuta () non avrà alcun effetto. L'immutabilità di una promessa consolidata è una caratteristica importante.

Le promesse JavaScript native non espongono gli stati delle promesse. Invece, dovresti considerare la promessa come una scatola nera. Solo la funzione responsabile della creazione della promessa avrà la conoscenza dello stato della promessa o l'accesso alla risoluzione o al rifiuto.

Ecco una funzione che restituisce una promessa che si risolverà dopo un determinato ritardo:

La nostra chiamata wait (3000) attenderà 3000ms (3 secondi), quindi registreremo "Hello!". Tutte le promesse compatibili con le specifiche definiscono un metodo .then () che viene utilizzato per passare i gestori che possono assumere il valore risolto o rifiutato.

Il costruttore promessa ES6 ha una funzione. Tale funzione accetta due parametri, resol () e reject (). Nell'esempio sopra, stiamo solo usando un metodo di risoluzione (), quindi ho lasciato rifiutare () dall'elenco dei parametri. Quindi chiamiamo setTimeout () per creare il ritardo e chiamiamo resol () al termine.

Puoi facoltativamente risolvere () o rifiutare () con i valori, che verranno passati alle funzioni di callback associate a .then ().

Quando rifiuto () con un valore, passo sempre un oggetto Error. In genere desidero due possibili stati di risoluzione: il normale percorso felice, o un'eccezione, qualsiasi cosa che impedisca il normale percorso felice. Passare un oggetto Error lo rende esplicito.

Importanti regole di promessa

Uno standard per le promesse è stato definito dalla comunità delle specifiche Promesse / A +. Esistono molte implementazioni conformi allo standard, incluso lo standard JavaScript promesse ECMAScript.

Le promesse che seguono le specifiche devono seguire un insieme specifico di regole:

  • Una promessa o "quindi" è un oggetto che fornisce un metodo .then () conforme agli standard.
  • Una promessa in sospeso può passare a uno stato soddisfatto o respinto.
  • Una promessa mantenuta o respinta viene stabilita e non deve passare a nessun altro stato.
  • Una volta risolta, una promessa deve avere un valore (che può essere indefinito). Quel valore non deve cambiare.

Le modifiche in questo contesto si riferiscono al confronto delle identità (===). Un oggetto può essere utilizzato come valore soddisfatto e le proprietà dell'oggetto possono mutare.

Ogni promessa deve fornire un metodo .then () con la seguente firma:

promise.then (
  onFulfilled ?: Funzione,
  onRejected ?: Funzione
) => Promessa

Il metodo .then () deve essere conforme a queste regole:

  • Sia onFulfilled () che onRejected () sono opzionali.
  • Se gli argomenti forniti non sono funzioni, devono essere ignorati.
  • onFulfilled () verrà chiamato dopo che la promessa è stata rispettata, con il valore della promessa come primo argomento.
  • onRejected () verrà chiamato dopo il rifiuto della promessa, con il motivo del rifiuto come primo argomento. Il motivo può 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 più volte con la stessa promessa. In altre parole, una promessa può essere utilizzata per aggregare i callback.
  • .then () deve restituire una nuova promessa, promise2.
  • Se onFulfilled () o onRejected () restituisce un valore x, e x è una promessa, promise2 si bloccherà con (assume lo stesso stato e valore di) x. Altrimenti, promessa2 sarà soddisfatta con il valore di x.
  • Se onFulfilled o onRejected genera un'eccezione e, promessa2 deve essere respinta con e come motivo.
  • Se onFulfilled non è una funzione e promise1 è adempiuto, promise2 deve essere adempiuto con lo stesso valore di promise1.
  • Se onRejected non è una funzione e promise1 viene rifiutato, promise2 deve essere rifiutato con lo stesso motivo di promise1.

Concatenamento promettente

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 ti consentono di imitare il normale comportamento try / catch del codice sincrono.

Come il codice sincrono, il concatenamento si tradurrà in una sequenza che viene eseguita in serie. In altre parole, puoi fare:

fetch (url)
  .then (processo)
  .poi (salvare)
  .catch (handleErrors)
;

Supponendo che ciascuna delle funzioni, fetch (), process () e save () restituiscano promesse, process () attenderà il completamento di fetch () prima di iniziare e save () attenderà il completamento di process () prima di iniziare. handleErrors () verrà eseguito solo se una delle precedenti promesse viene rifiutata.

Ecco un esempio di una complessa catena di promesse con più rifiuti:

Gestione degli errori

Tieni presente che le promesse hanno sia un successo che un gestore degli errori, ed è molto comune vedere il codice che fa questo:

save (). allora (
  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 ingoiato. Oops!

Per questo motivo, alcune persone considerano il codice sopra come un anti-pattern e raccomandano invece quanto segue:

salvare()
  .poi (handleSuccess)
  .catch (handleError)
;

La differenza è sottile, ma importante. Nel primo esempio, verrà rilevato un errore che ha origine nell'operazione save (), ma verrà eliminato un errore che ha origine nella funzione handleSuccess ().

Senza .catch (), un errore nel gestore del successo non viene rilevato.

Nel secondo esempio, .catch () gestirà i rifiuti da save () o handleSuccess ().

Con .catch (), vengono gestite entrambe le fonti di errore. (fonte diagramma)

Naturalmente, l'errore save () potrebbe essere un errore di rete, mentre l'errore handleSuccess () 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:

salvare()
  .poi(
    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 cancellare una promessa?

Una delle prime cose che spesso gli utenti delle nuove promesse si chiedono è come annullare una promessa. Ecco un'idea: respingi la promessa con "Annullato" come motivo. Se devi affrontarlo in modo diverso rispetto a un errore "normale", esegui la diramazione nel gestore degli errori.

Ecco alcuni errori comuni che le persone commettono quando annullano la propria promessa di annullamento:

Aggiunta di .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 si rompe quell'incapsulamento e si incoraggiano le persone a scrivere codice che manipola la promessa in luoghi che non dovrebbero conoscerlo. Evita gli spaghetti e le promesse non mantenute.

Dimenticando di pulire

Alcune persone intelligenti hanno capito che esiste un modo per utilizzare Promise.race () come meccanismo di cancellazione. Il problema è che il controllo della cancellazione è preso dalla funzione che crea la promessa, che è l'unico posto in cui è possibile condurre attività di pulizia adeguate, come la cancellazione dei timeout o la liberazione della memoria cancellando i riferimenti ai dati, ecc ...

Dimenticare di gestire una promessa di annullamento rifiutata

Sapevi che Chrome lancia messaggi di avviso su tutta la console quando ti dimentichi di gestire un rifiuto promettente? Oops!

Troppo complesso

La proposta ritirata di TC39 per la cancellazione ha proposto un canale di messaggistica separato per le cancellazioni. Ha anche usato un nuovo concetto chiamato token di annullamento. A mio avviso, la soluzione avrebbe notevolmente gonfiato le specifiche della promessa e l'unica caratteristica che avrebbe fornito a condizione che le speculazioni non supportino direttamente è la separazione di rigetti e cancellazioni, che, IMO, non è necessario per cominciare.

Vuoi fare il cambio a seconda che ci sia un'eccezione o una cancellazione? Si assolutamente. È questo il lavoro della promessa? Secondo me, no, non lo è.

Ripensare l'annullamento della promessa

In genere, 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. Potresti chiederti come potresti sapere se annulli o meno al momento della creazione della 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 rappresentare un potenziale valore in futuro ... oh, aspetta.

Il valore che trasmettiamo per rappresentare se annullare o meno potrebbe essere una promessa stessa. Ecco come potrebbe apparire:

Stiamo utilizzando l'assegnazione dei parametri predefinita per dirgli di non annullare per impostazione predefinita. Ciò rende il parametro di annullamento convenientemente facoltativo. Quindi impostiamo il timeout come in precedenza, ma questa volta acquisiamo l'ID del timeout in modo da poterlo cancellare in seguito.

Usiamo il metodo cancel.then () per gestire la cancellazione e la pulizia delle risorse. Questo verrà eseguito solo se la promessa viene annullata prima che abbia una possibilità di risolversi. Se annulli troppo tardi, hai perso l'occasione. Quel treno ha lasciato la stazione.

Nota: potresti chiederti a cosa serve la funzione noop (). La parola noop significa no-op, che significa una funzione che non fa nulla. Senza di essa, V8 genererà avvisi: UnhandledPromiseRejectionWarning: rifiuto della promessa non gestita. È una buona idea gestire sempre i rifiuti promettenti, anche se il gestore è un noop ().

Estrarre la promessa di annullamento

Questo va bene per un timer wait (), ma possiamo astrarre ulteriormente questa idea per incapsulare tutto ciò che devi ricordare:

  1. Rifiuta la promessa di annullamento per impostazione predefinita: non vogliamo annullare o generare errori se non viene accettata alcuna promessa di annullamento.
  2. Ricorda di eseguire la pulizia quando rifiuti per le cancellazioni.
  3. Ricorda che la pulizia di onCancel potrebbe generare un errore e che anche questo errore dovrà essere gestito. (Nota che la gestione degli errori è stata omessa nell'esempio di attesa sopra - è facile da dimenticare!)

Creiamo un'utilità di promessa cancellabile che puoi utilizzare per racchiudere qualsiasi promessa. Ad esempio, per gestire le richieste di rete, ecc ... La firma apparirà così:

speculazione (fn: SpecFunction, shouldCancel: Promise) => Promessa

SpecFunction è proprio come la funzione che passeresti al costruttore Promise, con un'eccezione: richiede un gestore onCancel ():

Funzione specifica (risoluzione: Funzione, rifiuto: Funzione, onCancel: Funzione) => Vuoto

Nota che questo esempio è solo un'illustrazione per darti un'idea di come funziona. Ci sono alcuni altri casi limite che devi prendere in considerazione. Ad esempio, in questa versione, verrà chiamato handleCancel se si annulla la promessa dopo che è stata risolta.

Ho implementato una versione di produzione mantenuta con casi limite coperti come libreria open source, Speculation.

Usiamo l'astrazione della libreria migliorata per riscrivere l'utilità wait () cancellabile di prima. Speculazione della prima installazione:

npm install - salva la speculazione

Ora puoi importarlo e usarlo:

Questo semplifica un po 'le cose, perché non devi preoccuparti di noop (), la rilevazione di errori in onCancel (), nella funzione o in altri casi limite. Questi dettagli sono stati sottratti dalla speculazione (). Dai un'occhiata e sentiti libero di usarlo in progetti reali.

Extra della promessa nativa di JS

L'oggetto Promise nativo 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 la ragione della prima promessa che rifiuta.
  • Promise.all () accetta un array (o qualsiasi iterabile) e restituisce una promessa che si risolve quando tutte le promesse nell'argomento iterabile si sono risolte o rifiuta con il motivo della promessa passata per prima che rifiuta.

Conclusione

Le promesse sono diventate parte integrante di diversi modi di dire in JavaScript, tra cui lo standard WHATWG Fetch utilizzato per la maggior parte delle richieste ajax moderne e lo standard Funzioni asincrone utilizzato per rendere il codice asincrono sembrare sincrono.

Le funzioni asincrone sono fase 3 al momento della stesura di questo documento, ma prevedo che presto diventeranno 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 JavaScript sviluppatori nel prossimo futuro.

Ad esempio, se stai usando Redux, ti suggerisco di dare un'occhiata a redux-saga: una libreria utilizzata per gestire gli effetti collaterali in Redux che dipende dalle funzioni asincrone presenti nella documentazione.

Spero che anche gli utenti promettenti esperti 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 classe e ereditarietà prototipale?
  • Che cos'è una funzione pura?
  • Cos'è la composizione delle funzioni?
  • Cos'è la programmazione funzionale?
  • Che cos'è una promessa?
  • Competenze trasversali
  1. Barbara Liskov; Liuba Shrira (1988). "Promesse: supporto linguistico per chiamate di procedura asincrone efficienti nei sistemi distribuiti". Atti della conferenza SIGPLAN 88 sulla programmazione e l'implementazione del linguaggio; Atlanta, Georgia, Stati Uniti, pagg. 260–267. ISBN 0–89791–269–1, pubblicato da ACM. Pubblicato anche in ACM SIGPLAN Avvisi, volume 23, numero 7, luglio 1988.
Inizia la tua lezione gratuita su EricElliottJS.com

Eric Elliott è l'autore dei libri, "Software di composizione" e "Programmazione di applicazioni JavaScript". Come co-fondatore di EricElliottJS.com e DevAnywhere.io, insegna agli sviluppatori le abilità di sviluppo software essenziali. Crea e consiglia team di sviluppo per progetti crittografici e ha contribuito alle esperienze software per Adobe Systems, Zumba Fitness, The Wall StreetJournal, ESPN, BBC e i migliori artisti discografici tra cui Usher, Frank Ocean, Metallica e molti altri.

Gode ​​di uno stile di vita remoto con la donna più bella del mondo.