Promouvoir les retours de retour des rappels

En ce qui concerne ces deux grandes sources: NZakas – Returning Promises in Promise Chains et MDN Promises , j'aimerais demander ce qui suit:

Chaque fois que nous renvoyons une valeur d'un gestionnaire de réalisation de promesses, comment cette valeur est-elle transmise à la nouvelle promesse retournée par ce même gestionnaire?

Par exemple,

let p1 = new Promise(function(resolve, reject) { resolve(42); }); let p2 = new Promise(function(resolve, reject) { resolve(43); }); let p3 = p1.then(function(value) { // first fulfillment handler console.log(value); // 42 return p2; }); p3.then(function(value) { // second fulfillment handler console.log(value); // 43 }); 

Dans cet exemple, p2 est une promesse. p3 est également une promesse provenant du gestionnaire de réalisation de p1 . Cependant p2 !== p3 . Au lieu de cela, p2 quelque sorte, résout magiquement à 43 (comment?) Et cette valeur est ensuite transmise au gestionnaire de réalisation de p3 . Même la phrase ici est déroutante.

Pourriez-vous m'expliquer ce qui se passe exactement ici? Je suis totalement confus sur ce concept.

Disons que le lancer à l'intérieur then() rappel renvoie la promesse de résultat avec un échec, et revenir à partir de then() rappel répond à la promesse de résultat avec une valeur de réussite.

 let p2 = p1.then(() => { throw new Error('lol') }) // p2 was rejected with Error('lol') let p3 = p1.then(() => { return 42 }) // p3 was fulfilled with 42 

Mais parfois, même dans le prolongement, nous ne savons pas si nous avons réussi ou non. Nous avons besoin de plus de temps.

 return checkCache().then(cachedValue => { if (cachedValue) { return cachedValue } // I want to do some async work here }) 

Cependant, si je fais un travail asynchrone là-bas, il serait trop tard pour return ou throw , n'est-ce pas?

 return checkCache().then(cachedValue => { if (cachedValue) { return cachedValue } fetchData().then(fetchedValue => { // Doesn't make sense: it's too late to return from outer function by now. // What do we do? // return fetchedValue }) }) 

C'est pourquoi Promises ne serait pas utile si vous ne pouviez pas résoudre une autre promesse .

Cela ne signifie pas que dans votre exemple, p2 deviendrait p3 . Ce sont des objets de promesse distincts. Cependant, en retournant p2 partir de ce moment- then() qui produit p3 vous dites "Je souhaite que p3 résolue à tout ce qui se résout, s'il réussit ou échoue".

En ce qui concerne la façon dont cela se produit, c'est spécifique à la mise en œuvre. En interne, vous pouvez penser then() en créant une nouvelle promesse. La mise en œuvre pourra le remplir ou le rejeter chaque fois qu'il l'aime. Normalement, il le remplira automatiquement ou le rejettera lorsque vous retournez:

 // Warning: this is just an illustration // and not a real implementation code. // For example, it completely ignores // the second then() argument for clarity, // and completely ignores the Promises/A+ // requirement that continuations are // run asynchronously. then(callback) { // Save these so we can manipulate // the returned Promise when we are ready let resolve, reject // Imagine this._onFulfilled is an internal // queue of code to run after current Promise resolves. this._onFulfilled.push(() => { let result, error, succeeded try { // Call your callback! result = callback(this._result) succeeded = true } catch (err) { error = err succeeded = false } if (succeeded) { // If your callback returned a value, // fulfill the returned Promise to it resolve(result) } else { // If your callback threw an error, // reject the returned Promise with it reject(error) } }) // then() returns a Promise return new Promise((_resolve, _reject) => { resolve = _resolve reject = _reject }) } 

Encore une fois, c'est très pseudo-code, mais montre l'idée derrière comment then() pourrait être implémenté dans les implémentations Promise.

Si nous voulons ajouter un support pour résoudre une promesse, il suffit de modifier le code pour avoir une succursale spéciale si le callback vous passez then() renvoyé une promesse:

  if (succeeded) { // If your callback returned a value, // resolve the returned Promise to it... if (typeof result.then === 'function') { // ...unless it is a Promise itself, // in which case we just pass our internal // resolve and reject to then() of that Promise result.then(resolve, reject) } else { resolve(result) } } else { // If your callback threw an error, // reject the returned Promise with it reject(error) } }) 

Permettez-moi de préciser à nouveau que ce n'est pas une mise en œuvre réelle de Promise et a de gros trous et des incompatibilités. Cependant, cela devrait vous donner une idée intuitive de la façon dont les bibliothèques Promise mettent en œuvre une solution à une promesse. Après vous être à l'aise avec l'idée, je vous recommanderais de voir comment les implémentations réelles de Promise fonctionnent.

Fondamentalement, p3 return une autre promesse: p2 . Ce qui signifie que le résultat de p2 sera transmis en tant que paramètre au prochain then rappel, dans ce cas il résout à 43 .

Chaque fois que vous utilisez le mot-clé, vous obtenez le résultat en tant que paramètre pour le rappel de la prochaine.

 let p3 = p1.then(function(value) { // first fulfillment handler console.log(value); // 42 return p2; }); 

Votre code :

 p3.then(function(value) { // second fulfillment handler console.log(value); // 43 }); 

Est égal à:

 p1.then(function(resultOfP1) { // resultOfP1 === 42 return p2; // // Returning a promise ( that might resolve to 43 or fail ) }) .then(function(resultOfP2) { console.log(resultOfP2) // '43' }); 

Btw, j'ai remarqué que vous utilisez la syntaxe ES6, vous pouvez avoir une syntaxe plus légère en utilisant la syntaxe de la flèche en graisse:

 p1.then(resultOfP1 => p2) // the `return` is implied since it's a one-liner .then(resultOfP2 => console.log(resultOfP2)); 

Dans cet exemple, p2 est une promesse. P3 est également une promesse provenant du gestionnaire de réalisation de p1. Cependant p2! == p3. Au lieu de cela, p2 résolue de manière magique à 43 (comment?) Et cette valeur est ensuite transmise au gestionnaire d'exécution de p3. Même la phrase ici est déroutante.

Une version simplifiée comment cela fonctionne (seul pseudocode)

 function resolve(value){ if(isPromise(value)){ value.then(resolve, reject); }else{ //dispatch the value to the listener } } 

Tout cela est bien plus compliqué puisque vous devez vous occuper, même si la promesse a déjà été résolue et quelques autres choses.

Je vais essayer de répondre à la question "pourquoi les rappels peuvent-ils rendre les Promise s" plus canoniques. Pour prendre un angle différent, je compare Promise s avec un type de conteneur moins complexe et déroutant – Array s.

Une Promise est un conteneur pour une valeur future. Un Array est un conteneur pour un nombre arbitraire de valeurs.

Nous ne pouvons pas appliquer les fonctions normales aux types de conteneurs:

 const sqr = x => x * x; const xs = [1,2,3]; const p = Promise.resolve(3); sqr(xs); // fails sqr(p); // fails 

Nous avons besoin d'un mécanisme pour les soulever dans le contexte d'un conteneur spécifique:

 xs.map(sqr); // [1,4,9] p.then(sqr); // Promise {[[PromiseValue]]: 9} 

Mais que se passe-t-il lorsque la fonction fournie elle-même renvoie un conteneur du même type?

 const sqra = x => [x * x]; const sqrp = x => Promise.resolve(x * x); const xs = [1,2,3]; const p = Promise.resolve(3); xs.map(sqra); // [[1],[4],[9]] p.then(sqrp); // Promise {[[PromiseValue]]: 9} 

sqra agit comme prévu. Il retourne simplement un conteneur imbriqué avec les valeurs correctes. Ce n'est évidemment pas très utile.

Mais comment interpréter le résultat de sqrp ? Si nous suivons notre propre logique, il devait s'agir de Promise {[[PromiseValue]]: Promise {[[PromiseValue]]: 9}} – mais ce n'est pas le cas. Alors, quelle magie se passe-t-il ici?

Pour reconstruire le mécanisme, nous devons simplement adapter notre méthode de map :

 const flatten = f => x => f(x)[0]; const sqra = x => [x * x]; const sqrp = x => Promise.resolve(x * x); const xs = [1,2,3]; xs.map(flatten(sqra)) 

flatten prend juste une fonction et une valeur, applique la fonction à la valeur et dégage le résultat, ce qui réduit une structure de matrice imbriquée d'un niveau.

Autrement dit, then dans le contexte de Promise s équivaut à une map combinée à flatten dans le contexte de Array s. Ce comportement est extrêmement important. Nous pouvons appliquer non seulement des fonctions normales à une Promise mais aussi des fonctions qui lui-même renvoient une Promise .

En fait, c'est le domaine de la programmation fonctionnelle. Une Promise est une implémentation spécifique d'une monade , then bind / chain et une fonction qui renvoie une Promise est une fonction monadique. Lorsque vous comprenez l'API Promise vous comprenez essentiellement toutes les monades.