Programmation asynchrone élégante avec des promesses

Bonjour mes amis!

Les promesses (promesses) sont une fonctionnalité relativement nouvelle de JavaScript qui vous permet de retarder l'exécution d'une action jusqu'à la fin de l'action précédente ou de répondre à l'exécution infructueuse de l'action. Cela contribue à la bonne détermination de la séquence des opérations asynchrones. Cet article explique comment fonctionnent les promesses, comment elles sont utilisées dans l'API Web et comment vous pouvez écrire votre propre promesse.

Conditions: connaissances de base en informatique, connaissance des bases de JS.
Objectif: comprendre quelles sont les promesses et comment elles sont utilisées.

Quelles sont les promesses?


Nous avons brièvement passé en revue les promesses dans le premier article du cours, ici nous les examinerons plus en détail.

En substance, une promesse est un objet qui représente un état intermédiaire d'une opération - elle «promet» que le résultat sera rendu à l'avenir. On ne sait pas exactement quand l'opération sera terminée et quand le résultat sera retourné, mais il y a une garantie que lorsque l'opération sera terminée, votre code fera quelque chose avec le résultat ou traitera correctement l'erreur.

En règle générale, le temps qu'une opération asynchrone prend (pas trop longtemps!) Nous intéresse moins que la possibilité d'une réponse immédiate à son achèvement. Et, bien sûr, il est bon de savoir que le reste du code n'est pas bloqué.

L'une des promesses les plus courantes sont les API Web qui renvoient des promesses. Regardons une application de chat vidéo hypothétique. L'application dispose d'une fenêtre avec une liste d'amis de l'utilisateur, en cliquant sur le bouton à côté du nom (ou avatar) de l'utilisateur démarre un appel vidéo.

Le gestionnaire de boutons appelle getUserMedia ()pour accéder à la caméra et au microphone de l'utilisateur. À partir du moment où getUserMedia () contacte l'utilisateur pour obtenir une autorisation (pour utiliser des appareils, quel appareil utiliser si l'utilisateur dispose de plusieurs microphones ou caméras, seul un appel vocal, entre autres), getUserMedia () attend non seulement la décision de l'utilisateur, mais aussi la libération appareils s'ils sont en cours d'utilisation. De plus, l'utilisateur peut ne pas répondre immédiatement. Tout cela peut entraîner des retards importants dans le temps.

Si une demande d'autorisation d'utiliser des périphériques est effectuée à partir du thread principal, le navigateur est bloqué jusqu'à ce que getUserMedia () soit terminé. C'est inadmissible. Sans promesses, tout dans le navigateur devient «non cliquable» jusqu'à ce que l'utilisateur donne la permission d'utiliser le microphone et la caméra. Par conséquent, au lieu d'attendre la décision d'un utilisateur et de renvoyer un MediaStream pour un flux créé à partir de sources (caméra et microphone), getUserMedia () renvoie une promesse que MediaStream traite dès qu'il est disponible.

Le code de l'application de chat vidéo peut ressembler à ceci:

function handle CallButton(evt){
    setStatusMessage('Calling...')
    navigator.mediaDevices.getUserMedia({video: true, audio: true})
    .then(chatStream => {
        selfViewElem.srcObject = chatStream
        chatStream.getTracks().forEach(track => myPeerConnection.addTrack(track, chatStream))
        setStatusMessage('Connected')
    }).catch(err => {
        setStatusMessage('Failed to connect')
    })
}

La fonction commence par appeler setStatusMessage (), affichant le message 'Calling ...', qui sert d'indicateur qu'une tentative est en cours pour effectuer un appel. Ensuite, getUserMedia () est appelé, demandant un flux contenant des pistes vidéo et audio. Une fois le flux formé, un élément vidéo est installé pour afficher le flux de la caméra appelé `` auto-vue '', des pistes audio sont ajoutées à WebRTC RTCPeerConnection , qui est une connexion à un autre utilisateur. Après cela, le statut est mis à jour sur «Connecté».

Si getUserMedia () échoue, le bloc catch est lancé. Il utilise setStatusMessage () pour afficher un message d'erreur.

Notez que l'appel à getUserMedia () est renvoyé même si le flux vidéo n'a pas encore été reçu. Même si la fonction handleCallButton () renvoyait le contrôle au code qui l'appelait, dès que getUserMedia () terminait l'exécution, elle appelait le gestionnaire. Jusqu'à ce que l'application «comprenne» que la diffusion a commencé, getUserMedia () sera en mode veille.

Remarque: vous pouvez en savoir plus à ce sujet dans l'article «Signaux et appels vidéo» . Cet article fournit un code plus complet que celui que nous avons utilisé dans l'exemple.

Problème des fonctions de rappel


Pour comprendre pourquoi les promesses sont une bonne solution, il est utile de regarder l'ancienne façon d'écrire des rappels pour voir quels étaient leurs problèmes.

Par exemple, pensez à commander une pizza. Une commande de pizza réussie se compose de plusieurs étapes qui doivent être effectuées dans l'ordre, l'une après l'autre:

  1. Choisissez la garniture. Cela peut prendre un certain temps si vous y pensez longtemps, et échouer si vous changez d'avis et commandez un curry.
  2. Nous passons une commande. La cuisson de la pizza prend un certain temps et peut échouer si le restaurant manque des ingrédients nécessaires.
  3. Nous obtenons une pizza et mangeons. La réception de la pizza peut échouer si, par exemple, nous ne pouvons pas payer la commande.

Un pseudocode à l'ancienne utilisant des fonctions de rappel pourrait ressembler à ceci:

chooseToppings(function(toppings){
    placeOrder(toppings, function(order){
        collectOrder(order, function(pizza){
            eatPizza(pizza)
        }, failureCallback)
    }, failureCallback)
}, failureCallback)

Un tel code est difficile à lire et à maintenir (il est souvent appelé «l'enfer des rappels» ou «l'enfer des rappels»). La fonction failureCallback () doit être appelée à chaque niveau d'imbrication. Il y a d'autres problèmes.

Nous utilisons des promesses


Les promesses rendent le code plus propre, plus facile à comprendre et à prendre en charge. Si nous réécrivons le pseudocode à l'aide de promesses asynchrones, nous obtenons ce qui suit:

chooseToppings()
.then(function(toppings){
    return placeOrder(toppings)
})
.then(function(order){
    return collectOrder(order)
})
.then(function(pizza){
    eatPizza(pizza)
})
.catch(failureCallback)

C'est beaucoup mieux - nous voyons ce qui se passe, nous utilisons un bloc .catch () pour gérer toutes les erreurs, la fonction ne bloque pas le flux principal (afin que nous puissions jouer à des jeux vidéo en attendant la pizza), chaque opération est garantie d'être effectuée une fois la précédente terminée. Puisque chaque promesse renvoie une promesse, nous pouvons utiliser la chaîne .then. Super, non?

En utilisant les fonctions fléchées, le pseudo-code peut être encore simplifié:

chooseToppings()
.then(toppings =>
    placeOrder(toppings)
)
.then(order =>
    collectOrder(order)
)
.then(pizza =>
    eatPizza(pizza)     
)
.catch(failureCallback)

Ou même comme ça:

chooseToppings()
.then(toppings => placeOrder(toppings))
.then(order => collectOrder(order))
.then(pizza => eatPizza(pizza))
.catch(failureCallback)

Cela fonctionne car () => x est identique à () => {return x}.

Vous pouvez même le faire (puisque les fonctions passent simplement des paramètres, nous n'avons pas besoin de superposition):

chooseToppings().then(placeOrder).then(collectOrder).then(eatPizza).catch(failureCallback)

Un tel code est plus difficile à lire et ne peut pas être utilisé avec des constructions plus complexes qu'en pseudocode.

Remarque: le pseudo-code peut toujours être amélioré en utilisant async / wait, qui sera discuté dans un prochain article.

À la base, les promesses sont similaires aux auditeurs d'événements, mais avec quelques différences:

  • Une promesse n'est tenue qu'une seule fois (succès ou échec). Il ne peut pas être exécuté deux fois ou passer de la réussite à l'échec ou vice versa une fois l'opération terminée.
  • Si la promesse est terminée et que nous ajoutons une fonction de rappel pour traiter son résultat, cette fonction sera appelée, malgré le fait que l'événement se soit produit avant l'ajout de la fonction.

Syntaxe de base des promesses: un exemple réel


Il est important de connaître les promesses car de nombreuses API Web les utilisent dans des fonctions qui exécutent des tâches potentiellement complexes. Travailler avec les technologies Web modernes implique l'utilisation de promesses. Plus tard, nous apprendrons à rédiger nos propres promesses, mais pour l'instant, regardons quelques exemples simples qui peuvent être trouvés dans l'API Web.

Dans le premier exemple, nous utilisons la méthode fetch () pour obtenir l'image du réseau, la méthode blob () pour convertir le contenu du corps de réponse en un objet Blob et afficher cet objet à l'intérieur de l'élément <img> . Cet exemple est très similaire à l'exemple du premier article , mais nous le ferons un peu différemment.

Remarque: l'exemple suivant ne fonctionnera pas si vous l'exécutez simplement à partir d'un fichier (c'est-à-dire en utilisant file: // URL). Vous devez l'exécuter via un serveur local ou utiliser des solutions en ligne telles que les pages Glitch ou GitHub .

1. Tout d'abord, téléchargez le code HTML et l' image que nous recevrons.

2. Ajoutez l'élément <script> à la fin de <body> .

3. À l'intérieur de l'élément <script>, ajoutez la ligne suivante:

let promise = fetch('coffee.jpg')

La méthode fetch (), qui prend l'URL de l'image comme paramètre, est utilisée pour obtenir l'image du réseau. En tant que deuxième paramètre, nous pouvons spécifier un objet avec des paramètres, mais pour l'instant nous nous limiterons à une option simple. Nous stockons la promesse retournée par fetch () dans la variable de promesse. Comme indiqué précédemment, une promesse est un objet qui représente un état intermédiaire - le nom officiel d'un état donné est en attente.

4. Pour travailler avec le résultat de la réussite de la promesse (dans notre cas, lorsque la réponse revient ), nous appelons la méthode .then (). La fonction de rappel à l'intérieur du bloc .then () (souvent appelé exécuteur) n'est lancée que lorsque la promesse se termine avec succès et que l'objet Response est renvoyé - ils disent que la promesse est terminée (remplie, remplie). La réponse est transmise en tant que paramètre.

Remarque. Le fonctionnement du bloc .then () est similaire au fonctionnement de l'écouteur d'événement AddEventListener (). Il n'est déclenché qu'après que l'événement se soit produit (après avoir rempli la promesse). La différence entre les deux est que .then () ne peut être appelé qu'une seule fois, tandis que l'écouteur est conçu pour plusieurs appels.

Après avoir reçu la réponse, nous appelons la méthode blob () pour convertir la réponse en un objet Blob. Cela ressemble à ceci:

response => response.blob()

... qui est une courte entrée pour:

function (response){
    return response.blob()
}

OK, assez de mots. Ajoutez ce qui suit après la première ligne:

let promise2 = promise.then(response => response.blob())

5. Chaque appel à .then () crée une nouvelle promesse. Ceci est très utile: puisque blob () retourne également une promesse, nous pouvons traiter l'objet Blob en appelant .then () sur la deuxième promesse. Étant donné que nous voulons faire quelque chose de plus compliqué que d'appeler la méthode et de renvoyer le résultat, nous devons encapsuler le corps de la fonction entre accolades (sinon, une exception sera levée):

Ajoutez ce qui suit à la fin du code:

let promise3 = promise2.then(myBlob => {

})

6. Complétons le corps de la fonction d'exécution. Ajoutez les lignes suivantes aux accolades:

let objectURL = URL.createObjectURL(myBlob)
let image = document.createElement('img')
image.src = objectURL
document.body.appendChild(image)

Ici, nous appelons la méthode URL.createObjectURL (), en la passant comme paramètre Blob, qui a renvoyé la deuxième promesse. Nous obtenons un lien vers l'objet. Ensuite, nous créons un élément <img>, définissons l'attribut src avec la valeur du lien d'objet et l'ajoutons au DOM pour que l'image apparaisse sur la page.

Si vous enregistrez le code HTML et le chargez dans un navigateur, vous verrez que l'image s'affiche comme prévu. Bon travail!

Remarque. Vous avez probablement remarqué que ces exemples sont quelque peu artificiels. Vous pouvez vous passer des méthodes fetch () et blob () et affecter l'URL <img> correspondante, coffee.jpg. Nous sommes allés de cette façon pour démontrer le travail avec les promesses avec un exemple simple.

Réponse d'échec


Nous avons oublié quelque chose - nous n'avons pas de gestionnaire d'erreurs au cas où l'une des promesses échouerait (sera rejetée). Nous pouvons ajouter un gestionnaire d'erreurs en utilisant la méthode .catch (). Faisons cela:

let errorCase = promise3.catch(e => {
    console.log('There has been a problem with your fetch operation: ' + e.message)
})

Pour voir cela en action, spécifiez la mauvaise URL de l'image et rechargez la page. Un message d'erreur apparaîtra dans la console.

Dans ce cas, si nous n'ajoutons pas le bloc .catch (), un message d'erreur sera également affiché dans la console. Cependant .catch () nous permet de gérer les erreurs comme nous le voulons. Dans une application réelle, votre bloc .catch () peut essayer de reprendre l'image ou afficher l'image par défaut, ou demander à l'utilisateur de sélectionner une autre image ou de faire autre chose.

Remarque. Regardez une démo en direct ( code source ).

Fusion de blocs


En fait, l'approche que nous avons utilisée pour écrire le code n'est pas optimale. Nous avons délibérément choisi cette voie pour que vous puissiez comprendre ce qui se passe à chaque étape. Comme indiqué précédemment, nous pouvons combiner des blocs .then () (et des blocs .catch ()). Notre code peut être réécrit comme suit (voir aussi simple-fetch-chained.html sur GitHub):

fetch('coffee.jpg')
.then(response => response.blob())
.then(myBlob => {
    let objectURL = URL.createObjectURL(myBlob)
    let image = document.createElement('img')
    image.src = objectURL
    document.body.appendChild(image)
})
.catch(e => {
    console.log('There has been a problem with your fetch operation: ' + e.message)
})

N'oubliez pas que la valeur renvoyée par la promesse est transmise en tant que paramètre au prochain exécuteur de fonction du bloc .then ().

Remarque. Les blocs .then () /. Catch () dans promesses sont des équivalents asynchrones du bloc synchrone try ... catch. N'oubliez pas: synchrone try ... catch ne fonctionnera pas en code asynchrone.

Promesse de conclusion terminologique


Faisons le point et rédigeons un petit guide que vous pourrez utiliser à l'avenir. Pour consolider les connaissances, nous vous recommandons de lire la section précédente plusieurs fois.

1. Lorsque la promesse est créée, ils disent qu'elle est dans un état d'attente.
2. Lorsque la promesse est retournée, ils disent qu'elle a été réalisée (résolue):

  1. 1. Une promesse accomplie avec succès est appelée tenue. Il renvoie la valeur qui peut être obtenue via la chaîne à partir de .then () à la fin de la promesse. La fonction d'exécution dans le bloc .then () contient la valeur renvoyée par la promesse.
  2. 2. Une promesse non remplie est appelée rejetée. Il renvoie la raison, le message d'erreur qui a conduit au rejet de la promesse. Cette raison peut être obtenue via le bloc .catch () à la fin de la promesse.

Exécutez le code après plusieurs promesses


Nous avons appris les bases de l'utilisation des promesses. Voyons maintenant des fonctionnalités plus avancées. La chaîne de .then () est très bien, mais que se passe-t-il si nous voulons appeler plusieurs blocs de promesses un par un?

Vous pouvez le faire en utilisant la méthode standard Promise.all (). Cette méthode prend un tableau de promesses comme paramètre et renvoie une nouvelle promesse lorsque toutes les promesses du tableau sont remplies. Cela ressemble à ceci:

Promise.all([a,b,c]).then(values => {
        ...
})

Si toutes les promesses sont remplies, l'exécuteur de tableau du bloc .then () sera passé en paramètre. Si au moins une des promesses n'est pas tenue, le bloc entier sera rejeté.

Cela peut être très utile. Imaginez que nous recevions des informations pour remplir dynamiquement l'interface utilisateur avec du contenu sur notre page. Dans de nombreux cas, il est plus raisonnable de recevoir toutes les informations et de les afficher sur la page plus tard que d'afficher les informations en plusieurs parties.

Regardons un autre exemple:
1. Téléchargez un modèle de page et placez la balise <script> avant la balise de fermeture </body>.

2. Téléchargez les fichiers sources ( coffee.jpg , tea.jpg et description.txt) ou remplacez-les par les vôtres.

3. Dans le script, nous définissons d'abord une fonction qui renvoie les promesses que nous transmettons à Promise.all (). Cela sera facile à faire si nous exécutons Promise.all () après avoir terminé les trois opérations fetch (). Nous pouvons faire ce qui suit:

let a = fetch(url1)
let b = fetch(url2)
let c = fetch(url3)

Promise.all([a, b, c]).then(values => {
    ...
})

Lorsque la promesse est remplie, la variable «values» contiendra trois objets Response, un pour chaque opération fetch () terminée.

Cependant, nous ne voulons pas cela. Cela n'a pas d'importance pour nous lorsque les opérations fetch () sont terminées. Ce que nous voulons vraiment, ce sont les données chargées. Cela signifie que nous voulons exécuter le bloc Promise.all () après avoir reçu un blob valide représentant des images et des chaînes de texte valides. Nous pouvons écrire une fonction qui fait cela; ajoutez ce qui suit à votre élément <script>:

function fetchAndDecode(url, type){
    return fetch(url).then(response => {
        if(type === 'blob'){
            return response.blob()
        } else if(type === 'text'){
            return response.text()
        }
    }).catch(e => {
        console.log('There has been a problem with your fetch operation ' + e.message)
    })
}

Cela semble un peu compliqué, alors passons en revue le code étape par étape:

1. Tout d'abord, nous déclarons une fonction et lui transmettons l'URL et le type du fichier résultant.

2. La structure de la fonction est similaire à celle que nous avons vue dans le premier exemple - nous appelons la fonction fetch () pour obtenir le fichier à une URL spécifique, puis transférons le fichier vers une autre promesse qui retourne le corps de réponse décodé (lecture). Dans l'exemple précédent, c'était toujours la méthode blob ().

3. Il y a deux différences:

  • Premièrement, la deuxième promesse de retour dépend du type de valeur. Dans la fonction d'exécution, nous utilisons l'instruction if ... else if pour renvoyer la promesse en fonction du type de fichier que nous devons décoder (dans ce cas, nous choisissons entre blob et texte, mais l'exemple peut être facilement étendu pour fonctionner avec d'autres types).
  • -, «return» fetch(). , (.. , blob() text(), , , ). , return .

4. À la fin, nous appelons la méthode .catch () pour gérer les erreurs qui peuvent se produire dans les promesses du tableau passé à .all (). Si l'une des promesses est rejetée, le bloc de capture nous fera savoir quelle promesse était le problème. Le bloc .all () s'exécutera toujours, mais il n'affichera pas les résultats pour lesquels il y a eu des erreurs. Si vous souhaitez que .all () soit rejeté, ajoutez un bloc .catch () à la fin.

Le code à l'intérieur de la fonction est asynchrone et basé sur des promesses, donc la fonction entière fonctionne comme une promesse.

4. Ensuite, nous appelons notre fonction trois fois pour démarrer le processus de réception et de décodage des images et du texte, et mettons chacune des promesses dans une variable. Ajoutez ce qui suit:

let coffee = fetchAndDecode('coffee.jpg', 'blob')
let tea = fetchAndDecode('tea.jpg', 'blob')
let description = fetchAndDecode('description.txt', 'text')

5. Ensuite, nous déclarons un bloc Promise.all () pour exécuter du code uniquement après que les trois promesses ont été remplies. Ajoutez un bloc avec une fonction d'exécuteur vide à l'intérieur de .then ():

Promise.all([coffee, tea, description]).then(values => {

})

Vous pouvez voir que cela prend un tableau de promesses comme paramètre. L'entrepreneur ne commencera que lorsque les trois promesses auront été tenues; lorsque cela se produit, le résultat de chaque promesse (le corps de réponse décodé) sera placé dans un tableau, cela peut être représenté comme [résultats du café, résultats du thé, résultats de la description].

6. Enfin, ajoutez ce qui suit à l'exécuteur (ici, nous utilisons un code synchrone assez simple pour mettre les résultats dans des variables (création d'objets URL à partir de blob) et afficher des images et du texte sur la page):

console.log(values)
//       
let objectURL1 = URL.createObjectURL(values[0])
let objectURL2 = URL.createObjectURL(values[1])
let descText = values[2]

//  
let image1 = document.createElement('img')
let image2 = document.createElement('img')
image1.src = objectURL1
image2.src = objectURL2
document.body.appendChild(image1)
document.body.appendChild(image2)

//  
let para = document.createElement('p')
para.textContent = descText
document.body.appendChild(para)

Enregistrez les modifications et rechargez la page. Vous devriez voir que tous les composants de l'interface utilisateur se chargent, bien que cela ne semble pas très attrayant.

Remarque. Si vous rencontrez des difficultés, vous pouvez comparer votre version du code avec la nôtre - voici une démo en direct et le code source .

Remarque. Afin d'améliorer ce code, vous pouvez parcourir les éléments affichés, les recevoir et les décoder, puis parcourir les résultats dans Promise.all (), en lançant différentes fonctions pour afficher chaque résultat en fonction de son type. Cela vous permettra de travailler avec n'importe quel nombre d'éléments.

De plus, vous pouvez déterminer le type du fichier résultant sans avoir à spécifier explicitement la propriété type. Cela, par exemple, peut être fait avecresponse.headers.get ('content-type') pour vérifier l'en - tête HTTP Protocol Content-Type .

Exécutez le code après avoir rempli / rejeté la promesse


Souvent, vous devrez peut-être exécuter du code une fois la promesse terminée, qu'elle ait été exécutée ou rejetée. Auparavant, nous devions inclure le même code dans le bloc .then () et le bloc .catch (), par exemple:

myPromise
.then(response => {
    doSomething(response)
    runFinalCode()
})
.catch(e => {
    returnError(e)
    runFinalCode()
})

Dans les navigateurs modernes, la méthode .finally () est disponible, ce qui vous permet d'exécuter le code une fois la promesse terminée, ce qui évite la répétition et rend le code plus élégant. Le code de l'exemple précédent peut être réécrit comme suit:

myPromise
.then(response => {
    doSomething(response)
})
.catch(e => {
    returnError(e)
})
.finally(() => {
    runFinalCode()
})

Vous pouvez voir l'utilisation de cette approche avec un exemple réel - la démonstration en direct promise-finally.html ( code source ). Cela fonctionne de la même manière que Promise.all () de l'exemple précédent, sauf que nous ajoutons enfin () à la fin de la chaîne dans la fonction fetchAndDecode ():

function fetchAndDecode(url, type){
    return fetch(url).then(response => {
        if(type === 'blob'){
            return response.blob()
        } else if(type === 'text'){
            return response.text()
        }
    }).catch(e => {
        console.log(`There has been a problem with your fetch operation for resource "${url}": ${e.message}`)
    }).finally(() => {
        console.log(`fetch attempt for "${url}" finished.`)
    })
}

Nous recevrons un message indiquant la fin de chaque tentative d'obtention du fichier.

Remarque. then () / catch () / finally () est l'équivalent asynchrone de synchrone try () / catch () / finally ().

Rédiger votre propre promesse


La bonne nouvelle est que vous savez déjà comment procéder. Lorsque vous combinez plusieurs promesses avec .then () ou les combinez pour fournir des fonctionnalités spécifiques, vous créez vos propres fonctions asynchrones et basées sur les promesses. Prenons par exemple notre fonction fetchAndDecode () de l'exemple précédent.

La combinaison de diverses API basées sur des promesses pour créer des fonctionnalités spécifiques est la façon la plus courante de travailler avec des promesses, ce qui montre la flexibilité et la puissance des API modernes. Cependant, il existe un autre moyen.

Promesse du constructeur ()


Vous pouvez créer votre propre promesse à l'aide du constructeur Promise (). Cela peut être nécessaire dans une situation où vous avez du code de l'ancienne API asynchrone que vous devez «surdimensionner». Ceci est fait afin de pouvoir travailler simultanément à la fois avec du code, des bibliothèques ou des «frameworks» existants et anciens, et avec un nouveau code basé sur des promesses.

Regardons un exemple simple - ici, nous encapsulons l'appel setTimeout () dans une promesse - cela démarrera la fonction en 2 secondes, qui remplira la promesse (en utilisant l'appel resolver () passé) avec la chaîne «Success!».

let timeoutPromise = new Promise((resolve, reject) => {
    setTimeout(function(){
        resolve('Success!')
    }, 2000)
})

resolver () et rejeter () sont des fonctions appelées à remplir ou à rejeter une nouvelle promesse. Dans ce cas, la promesse est remplie avec la chaîne "Success!".

Lorsque vous appelez cette promesse, vous pouvez y ajouter un bloc .then () pour continuer à travailler avec la ligne «Success!». Nous pouvons donc sortir une ligne dans le message:

timeoutPromise
.then((message) => {
    alert(message)
})

... ou alors:

timeoutPromise.then(alert)

Regardez une démo en direct ( code source ).

L'exemple donné n'est pas très flexible - la promesse ne peut être remplie qu'avec une simple ligne, nous n'avons pas de gestionnaire d'erreur - rejette () (en fait, setTimeout () n'a pas besoin d'un gestionnaire d'erreur, donc dans ce cas, cela n'a pas d'importance).

Remarque. Pourquoi résoudre () plutôt que réaliser ()? Pour le moment, la réponse est la suivante: c'est difficile à expliquer.

Nous travaillons avec un rejet d'une promesse


Nous pouvons créer une promesse rejetée en utilisant la méthode rejeter () - tout comme resolver (), rejeter () prend une valeur simple, mais contrairement à resolver (), la valeur simple n'est pas le résultat, mais la raison du rejet, c'est-à-dire une erreur transmise au bloc .catch ().

Développons l'exemple précédent en ajoutant une condition de rejet d'une promesse, ainsi que la possibilité de recevoir des messages autres qu'un message de réussite.

Prenez l' exemple précédent et réécrivez-le comme ceci:

function timeoutPromise(message, interval){
    return new Promise((resolve, reject) => {
        if(message === '' || typeof message !== 'string'){
            reject('Message is empty or not a string')
        } else if(interval < 0 || typeof interval !== number){
            reject('Interval is negative or not a number')
        } else{
            setTimeout(function(){
                resolve(message)
            }, interval)
        }
    })
}

Ici, nous passons deux arguments à la fonction - message et intervalle (temporisation). Dans une fonction, nous retournons un objet Promise.

Dans le constructeur Promise, nous effectuons plusieurs vérifications en utilisant les structures if ... else:

  1. Tout d'abord, nous vérifions le message. S'il est vide ou n'est pas une chaîne, nous rejetons la promesse et signalons une erreur.
  2. Deuxièmement, nous vérifions le délai. S'il s'agit d'un nombre négatif ou non, nous rejetons également la promesse et signalons une erreur.
  3. Enfin, si les deux arguments sont OK, affichez le message après un certain temps (intervalle) à l'aide de setTimeout ().

Puisque timeoutPromise () renvoie une promesse, nous pouvons y ajouter .then (), .catch (), etc. pour améliorer sa fonctionnalité. Faisons cela:

timeoutPromise('Hello there!', 1000)
.then(message => {
    alert(message)
}).catch(e => {
    console.log('Error: ' + e)
})

Après avoir enregistré les modifications et exécuté le code, vous verrez un message après une seconde. Essayez maintenant de passer une chaîne vide comme message ou un nombre négatif comme intervalle. Vous verrez un message d'erreur à la suite du rejet de la promesse.

Remarque. Vous pouvez trouver notre version de cet exemple sur GitHub - custom-promise2.html ( source ).

Exemple plus réaliste.


L'exemple que nous avons examiné facilite la compréhension du concept, mais il n'est pas vraiment asynchrone. L'asynchronie dans l'exemple est simulée à l'aide de setTimeout (), bien qu'elle montre encore l'utilité des promesses pour la création de fonctions avec un flux de travail raisonnable, une bonne gestion des erreurs, etc.

Un exemple d'une application asynchrone utile qui utilise le constructeur Promise () est la bibliothèque idb (IndexedDB with usability)Jake Archibald. Cette bibliothèque vous permet d'utiliser l'API IndexedDB (basée sur les fonctions de rappel d'API pour le stockage et la récupération de données côté client), écrite dans l'ancien style, avec des promesses. Si vous regardez le fichier de bibliothèque principal, vous verrez qu'il utilise les mêmes techniques que nous avons examinées ci-dessus. Le code suivant convertit le modèle de requête de base utilisé par de nombreuses méthodes IndexedDB en un modèle compatible avec les promesses:

function promisifyRequest(request){
    return new Promise(function(resolve, reject){
        request.onsuccess = function(){
            resolve(request.result)
        }

        request.onerror = function(){
            reject(request.error)
        }
    })
}

Cela ajoute quelques gestionnaires d'événements qui remplissent ou rejettent la promesse, selon le cas:

  • Lorsque la demande aboutit , le gestionnaire onsuccess remplit la promesse avec le résultat de la demande.
  • Lorsque la demande échoue , le gestionnaire onerror rejette la promesse avec l'erreur de la demande.

Conclusion


Les promesses sont un excellent moyen de créer des applications asynchrones lorsque nous ne savons pas quelle valeur la fonction renverra ni combien de temps cela prendra. Ils vous permettent de travailler avec une séquence d'opérations asynchrones sans utiliser de fonctions de rappel profondément imbriquées, et ils prennent en charge le style de gestion des erreurs de l'instruction try ... catch synchrone.

Les promesses sont prises en charge par tous les navigateurs modernes. La seule exception est Opera Mini et IE11 et ses versions antérieures.

Dans cet article, nous avons considéré loin de toutes les caractéristiques des promesses, seules les plus intéressantes et utiles. Vous apprendrez à connaître d'autres fonctionnalités et techniques si vous souhaitez en savoir plus sur les promesses.

La plupart des API Web modernes sont basées sur des promesses, donc les promesses doivent être connues. Parmi ces API Web, nous pouvons nommer WebRTC , Web Audio API , Media Capture and Streams , etc. Les promesses deviendront de plus en plus populaires, donc leur étude et leur compréhension sont une étape importante vers la maîtrise du JavaScript moderne.

Voir également «Erreurs courantes dans les promesses JavaScript que tout le monde devrait connaître» .

Merci de votre attention.

La critique constructive est la bienvenue.

All Articles