web-dev-qa-db-fra.com

Promise callbacks retournant des promesses

En ce qui concerne ces deux grandes sources: NZakas - Retourner les promesses dans les chaînes de promesses et Les promesses de MDN , j'aimerais poser la question suivante:

Chaque fois que nous renvoyons une valeur d'un gestionnaire d'exécution des promesses, comment cette valeur est-elle transmise à la nouvelle promesse renvoyé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 d'exécution de p1. Cependant p2 !== p3. Au lieu de cela, p2 Se résout comme par magie en 43 (Comment?) Et cette valeur est ensuite transmise au gestionnaire d'exécution 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.

53
kstratis

Supposons que le lancement de then() callback rejette la promesse de résultat avec un échec et que le retour de then() callback remplisse la promesse de résultat avec une valeur de succès.

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 la suite, 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
})

Toutefois, si j’ai 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 les promesses ne seraient pas utiles si vous ne pouviez pas résoudre une autre promesse .

Cela ne signifie pas que, dans votre exemple, p2 devientp3. Ce sont des objets Promise séparés. Cependant, en renvoyant p2 À partir de then() qui produit p3, Vous dites "Je veux que p3 Soit résolu en quelle que soit la résolution de p2, qu'elle réussisse ou non "

Quant à comment cela se produit, cela dépend de la mise en oeuvre. En interne, vous pouvez penser à then() comme créant une nouvelle promesse. L'implémentation sera capable de la remplir ou de la rejeter quand bon lui semble. Normalement, il le remplira ou le rejettera automatiquement à votre retour:

// 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, il s’agit bien de pseudo-code mais montre l’idée derrière la façon dont then() pourrait être implémenté dans les implémentations de Promise.

Si nous voulons ajouter un support pour la résolution à une promesse, nous avons juste besoin de modifier le code pour avoir une branche spéciale si le callback que vous passez à then() a retourné 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 qu'il ne s'agit pas d'une implémentation réelle de Promise, mais présente de gros trous et des incompatibilités. Toutefois, cela devrait vous donner une idée intuitive de la manière dont les bibliothèques Promise implémentent la résolution en promesse. Une fois que vous êtes à l'aise avec l'idée, je vous recommanderais de jeter un coup d'œil à la façon dont les implémentations Promise réelles gérez ceci .

38
Dan Abramov

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

Chaque fois que vous utilisez le mot-clé return, vous transmettez le résultat en tant que paramètre au prochain rappel de then.

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 claire en utilisant la syntaxe grosse flèche:

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

Dans cet exemple, p2 est une promesse. p3 est également une promesse provenant du gestionnaire d'exécution de p1. Cependant p2! == p3. Au lieu de cela, p2 se résout magiquement à 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 ça marche (pseudo-code seulement)

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

le tout est bien plus compliqué puisque vous devez faire attention, si la promesse a déjà été résolue et quelques autres choses.

4
Thomas

Je vais essayer de répondre à la question "Pourquoi then callbacks peuvent renvoyer Promises eux-mêmes" plus canoniques. Pour prendre un angle différent, je compare Promises à un type de conteneur moins complexe et prêtant à confusion - Arrays.

Un 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 placer 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 renvoie elle-même 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 renvoie simplement un conteneur imbriqué avec les valeurs correctes. Ce n'est évidemment pas très utile cependant.

Mais comment interpréter le résultat de sqrp? Si nous suivons notre propre logique, ce devait être quelque chose comme Promise {[[PromiseValue]]: Promise {[[PromiseValue]]: 9}} - mais ce n'est pas. Alors, quelle magie se passe ici?

Pour reconstruire le mécanisme, nous devons simplement adapter un peu notre méthode 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écompresse le résultat, réduisant ainsi la structure d'un tableau imbriqué d'un niveau.

Autrement dit, then dans le contexte de Promises équivaut à map combiné à flatten dans le contexte de Arrays. Ce comportement est extrêmement important. Nous pouvons appliquer non seulement des fonctions normales à un Promise mais également des fonctions qui retournent elles-mêmes un Promise.

En fait, c'est le domaine de la programmation fonctionnelle. Un Promise est une implémentation spécifique d'un monad, then est bind/chain et une fonction qui retourne un Promise est une fonction monadique. Lorsque vous comprenez l'API Promise, vous comprenez en gros toutes les monades.

3
user6445533