2
votes

Pourquoi une nouvelle promesse est-elle résolue plus tôt avec p.then (résoudre) qu'avec resolution (p)?

La différence entre le code n ° 1 et le code n ° 2 est la suivante: le code n ° 1 utilise résoudre (p) et le code n ° 2 utilise p. puis (() => résoudre ()) . Je m'attendrais à ce que la séquence de sortie soit invariante, mais ils génèrent une séquence différente. Je ne comprends pas pourquoi.

Code # 1: resolve(p)

tick:a
after:await
tick:b
tick:c

const p = Promise.resolve();

new Promise((resolve) => {
    p.then(()=>resolve());    // <---
}).then(() => {
    console.log('after:await');
});

p.then(() => console.log('tick:a'))
    .then(() => console.log('tick:b'))
    .then(() => console.log('tick:c'));

Code # 2: p. puis (() => résoudre ())

tick:a
tick:b
after:await
tick:c

const p = Promise.resolve();

new Promise((resolve) => {
    resolve(p);    // <---
}).then(() => {
    console.log('after:await');
});

p.then(() => console.log('tick:a'))
    .then(() => console.log('tick:b'))
    .then(() => console.log('tick:c'));

Pourquoi l'ordre de sortie est-il différent?


5 commentaires

Lorsqu'il y a plusieurs objets Promise en cours de lecture et prêts à être envoyés, le moteur d'exécution peut entrelacer les travaux comme bon lui semble. Pour toute single Promise, les rappels doivent être appelés dans l'ordre, mais les rappels pour d'autres instances de Promise peuvent être entrelacés (ou non).


résoudre résout une promesse, puis attache une fonction à exécuter après la résolution de la promesse. Ils sont assez différents.


@Pointy, il semble que bien que les spécifications Promise / A + permettent d'entrelacer librement les jobs Promise, les spécifications EcmaScript définissent un ordre déterministe: les jobs sont mis en file d'attente FIFO.


@trincot oui, je pense que vous avez raison; la spécification est déroutante mais il semble que la file d'attente des travaux pour les promesses est toujours la file d'attente "PromiseJobs" , ce qui implique que tous les travaux .then () etc. vont dans une grande file d'attente . J'ai été déconcerté par le langage qui dit que les travaux des autres files d'attente de travaux peuvent être entrelacés.


Je ne créerais pas délibérément du code qui dépendait de l'ordre des rappels de promesse, mais je suppose que ce serait exaspérant s'il était vraiment imprévisible car des dépendances par inadvertance provoqueraient des bogues difficiles à reproduire.


4 Réponses :


0
votes

Une promesse est dans l'un de ces états:

    -pending: état initial, ni rempli ni rejeté.
    -fulfilled: signifie que l'opération s'est terminée avec succès.
    -rejected: signifie que l'opération a échoué.
Une promesse en attente peut être remplie avec une valeur ou rejetée avec une raison (erreur). Lorsque l'une de ces options se produit, les gestionnaires associés mis en file d'attente par la méthode then d'une promesse sont appelés. Consultez ceci pour plus de détails

Maintenant, dans votre cas particulier, vous utilisez "Promise.resolve ()" probablement pour créer un nouvel objet de promesse mais ce qu'il fait, c'est qu'il crée une promesse déjà résolue sans valeur. Ainsi, votre objet de promesse "p" est résolu lors de sa création et le reste du code où vous le résolvez n'a littéralement aucun effet autre que de placer le "after: wait" dans une file d'attente de gestionnaire. Veuillez vous reporter à la Boucle d'événement avec zéro retard . La sortie des deux codes est différente en fonction du moment où le "console.log" est placé dans la pile d'appels et non de la manière dont vous l'écrivez.

La bonne façon de procéder peut être :

var promise1 = new Promise(function(resolve, reject) {
 setTimeout(function() {
  resolve('foo');
 }, 300);
});

promise1.then(function(value) {
 console.log(value);// expected output: "foo"
});

console.log(promise1);// expected output: [object Promise]

3 commentaires

Je ne pense pas que la question était de trouver "la bonne façon de procéder". Il demande pourquoi la sortie est différente. Bien sûr, cela dépend du moment où console.log est mis dans la pile d'appels, mais c'est vraiment la question: pourquoi est-il mis dans un ordre différent dans les deux versions de code?


Le lien Event Loop with Zero Delay est suffisant pour obtenir les bases directement. Vous pouvez en savoir plus sur ici


bien que ces liens soient utiles pour comprendre les mécanismes de l'asynchronie dans JS, je pense que l'OP en est tout à fait conscient. Ces ressources n'expliquent cependant pas la différence spécifique de séquençage que OP demande.



2
votes

Dans vos deux réponses, la chaîne de promesse1 et la chaîne de promesse2 peuvent être entrelacées différemment. Mais, tick: a, tick: b, tick: c seront affichés dans cet ordre, tick: a avant tick: b, et tick: b avant tick: c. after: await peut être affiché n'importe où entre les deux.

Pour ce que fait votre code.

const p = Promise.resolve();

new Promise((resolve) => {
    // Here you are calling then which if promise p has been fulfilled
    // will call the callback you passed as an argument, which then
    // will eventually cause the outer promise to enter a state of
    // fulfilled triggering a call to the next 'then' provided in the part of the chain. 
    p.then(()=>resolve());
}).then(() => {
    console.log('after:await');
});


p.then(() => console.log('tick:a'))
    .then(() => console.log('tick:b'))
    .then(() => console.log('tick:c'));

// Returns a resolved promise object
// Which is equivalent to const p = new Promise(resolve => resolve());
const p = Promise.resolve();

// For Reference Call This Promise Chain 1

new Promise((resolve) => {
    // Fulfills the promise with the promise object p 
    resolve(p);  // (1)
}).then(() => {
    console.log('after:await');
});

For Reference Promise Chain 2
p.then(() => console.log('tick:a'))
    .then(() => console.log('tick:b'))
    .then(() => console.log('tick:c'));


1 commentaires

"after: await peut être affiché n'importe où entre les deux." : il s'avère que la spécification EcmaScript dicte un ordre spécifique, ne laissant aucune place pour d'autres ordres de sortie que la façon dont il fonctionne dans le code de l'Asker.



1
votes

La méthode Promise.resolve() renvoie un objet Promise qui est résolu avec une valeur donnée. Si la valeur est une promesse, cette promesse est retournée; si la valeur est un thenable (c'est-à-dire a une méthode "then" ), la promesse retournée "suivra" cette table alors en adoptant son état éventuel; sinon, la promesse retournée sera remplie avec la valeur. Cette fonction aplatit les couches imbriquées d'objets de type promesse (par exemple, une promesse qui se résout en une promesse qui se résout en quelque chose) en une seule couche.

Veuillez vous référer ici pour plus d'informations sur Promise.resolve ().

La différence dans la sortie de vos deux codes est due au fait que les gestionnaires puis sont appelés de manière asynchrone.

Lors de l'utilisation d'une promesse résolue, le bloc «alors» sera déclenché instantanément, mais ses gestionnaires seront déclenchés de manière asynchrone.

Veuillez vous référer ici pour plus d'informations sur le comportement des gestionnaires puis .


1 commentaires

Puis-je vous suggérer de formater le premier paragraphe sous forme de citation, car il s'agit d'une copie littérale de l'article MDN auquel vous faites référence. Deuxièmement, vous écrivez "La différence dans la sortie de vos deux codes est due au fait que les gestionnaires then sont appelés de manière asynchrone." . Mais cela n'explique pas la différence de sortie. Les deux codes ont cet effet asynchrone. En fait, le premier code a un puis de plus, mais la sortie correspondante arrive plus tôt (pas plus tard) que dans la deuxième version.



4
votes

C'est en fait une question très intéressante, car les Promise / A + specs permettraient à la première version du code de produire le même résultat que la deuxième version du code.

On pourrait rejeter la question en disant que l'implémentation de Promise ne dit rien sur la façon dont résoudre (p) serait implémenté. Ceci est une déclaration vraie lorsque l'on regarde la spécification Promise / A +, citant sa préface:

la spécification principale Promises / A + ne traite pas de la façon de créer, de remplir ou de rejeter des promesses, ...

Mais la spécification EcmaScript pour Promises (section 25.4 ) est bien plus détaillée que la spécification Promise / A + et exige que des" travaux "soient ajoutés à l'arrière de la file d'attente des travaux correspondante - ce qui pour les règlements de promesse est le PromiseJobs file d'attente ( 25.4.1.3.2 et 8.4 ): cela détermine un ordre spécifique:

Files d'attente de travaux requises

[...]
PromiseJobs : Emplois qui sont des réponses au règlement d'une promesse

[...]

Les enregistrements PendingJob d'une seule file d'attente de travaux sont toujours lancés dans l'ordre FIFO

Il définit également que résoudre (p) - lorsque p est une table alors - mettra d'abord une tâche sur la file d'attente qui effectuera l'appel interne nécessaire de la méthode p. puis . Ceci n'est pas effectué immédiatement. Pour citer la note dans les spécifications EcmaScript à 25.4.2.2 a >:

Ce processus doit avoir lieu en tant que Job pour garantir que l'évaluation de la méthode then se produit après l'évaluation de tout code environnant.

Cette instruction est illustrée par l'ordre de sortie dans l'extrait suivant:

const p1 = Promise.resolve();
const p2 = new Promise((resolve) => resolve(p1));
const p3 = p2.then(() => console.log('after:await'));
const p4 = p1.then(() => console.log('tick:a'));
const p5 = p4.then(() => console.log('tick:b'))
const p6 = p5.then(() => console.log('tick:c'));

Lorsque nous utilisons l'appel de méthode p1.then (résoudre) au lieu de resolution (p1) , nous obtenons l'ordre inverse:

const p1 = Promise.resolve();

// Wrap the `p1.then` method, so we can log something to the console:
const origThen = p1.then;
p1.then = function(...args) {
    console.log("The p1.then method is called synchronously now");
    origThen.call(this, ...args);
};
const p2 = new Promise(resolve => {
    p1.then(resolve);
    console.log("Code that follows is executed synchronously, after p1.then is");
});

Votre code

Ce qui précède explique vraiment les différents ordres de sortie que vous obtenez. Voici comment la première version de code séquence les actions. Tout d'abord, permettez-moi de réécrire un peu ceci afin que la plupart des promesses impliquées aient un nom:

const p1 = Promise.resolve();

// Wrap the `p1.then` method, so we can log something to the console:
const origThen = p1.then;
p1.then = function(...args) {
    console.log("The p1.then method is called asynchronously when triggered by resolve(p1)");
    origThen.call(this, ...args);
};
const p2 = new Promise(resolve => {
    resolve(p1);
    console.log("Code that follows is executed synchronously, before p1.then is");
});

Maintenant, une fois le code principal synchrone exécuté jusqu'à la fin, seul p1 a un état résolu, et deux travaux sont présents dans la file d'attente des travaux (file d'attente des micro-tâches), un suite à résoudre (p1) et une seconde à cause de p1. puis:

  1. Selon 25.4.2.2 , la méthode puis de p1 est appelée en lui passant la fonction interne [[résoudre]] liée à p2 . Les internes de p1.then savent que p1 est résolu et mettent encore un autre travail dans la file d'attente pour résoudre réellement p2 !

  2. Le rappel avec "tick: a" est exécuté et la promesse p4 est marquée comme remplie, ajoutant un nouveau travail dans la file d'attente des travaux. Il y a maintenant 2 nouveaux travaux dans la file d'attente, qui sont traités dans l'ordre:

  3. Le travail de l'étape 1 est exécuté: p2 est maintenant résolu. Cela signifie qu'un nouveau travail est mis en file d'attente pour appeler le ou les rappels puis correspondants

  4. Le job de l'étape 2 est exécuté: le callback avec "tick: b" est exécuté

Ce n'est que plus tard que le job ajouté à l'étape 3 sera exécuté, ce qui appellera le callback avec "after: await".

Donc, en conclusion. Dans EcmaScript, un résoudre (p) , où p est un thenable implique un travail asynchrone, qui lui-même déclenche un autre travail asynchrone pour notifier l'exécution.

Le rappel puis , qui différencie la deuxième version de code, n'aura besoin que d'un un travail asynchrone pour être appelé, et ainsi il se produit avant la sortie de "tick: b".


3 commentaires

Personnellement, je pense que c'était une mauvaise décision de dicter ce comportement dans la spécification, ils auraient dû laisser le soin aux implémentations. Dans tous les cas, cela a été fait uniquement pour que toutes les implémentations soient cohérentes dans leur entrelacement de tâches concurrentes, et non pour que quiconque essaie de se fier à cet ordre particulier dans son code.


@Bergi bien que je convienne que ce fut une décision malheureuse, cela facilite la vie du consommateur en garantissant une commande pour une séquence particulière (en supposant que la mise en œuvre est conforme, ce qui est une question entièrement différente). Je crois qu'il existe une situation analogue où Array.prototype.sort () aurait pu être spécifié comme stable ou simplement dépendant de l'implémentation, et bien que je suis d'accord avec le choix de la spécification de permettre à la stabilité d'être dépendante de l'implémentation , il existe de nombreuses bases de code qui reposent sur des implémentations choisissant un algorithme de tri stable. Ce fut un sujet brûlant dans V8 pendant un certain temps.


@PatrickRoberts Bien sûr, mais alors qu'un tri stable est une chose raisonnable à attendre, aucune base de code ne devrait jamais dépendre de la séquence particulière de promesses simultanées. Spécifier ces détails les obligeait à mettre à jour les spécifications à quelques reprises maintenant, où par exemple La V8 a optimisé une coche inutile sur leur implémentation wait . (Et rien ne s'est cassé).