web-dev-qa-db-fra.com

Quel est l'ordre d'exécution dans les promesses javascript

Je voudrais m'expliquer l'ordre d'exécution de l'extrait suivant qui utilise des promesses javascript.

Promise.resolve('A')
  .then(function(a){console.log(2, a); return 'B';})
  .then(function(a){
     Promise.resolve('C')
       .then(function(a){console.log(7, a);})
       .then(function(a){console.log(8, a);});
     console.log(3, a);
     return a;})
  .then(function(a){
     Promise.resolve('D')
       .then(function(a){console.log(9, a);})
       .then(function(a){console.log(10, a);});
     console.log(4, a);})
  .then(function(a){
     console.log(5, a);});
console.log(1);
setTimeout(function(){console.log(6)},0);

Le résultat est:

1
2 "A"
3 "B"
7 "C"
4 "B"
8 undefined
9 "D"
5 undefined
10 undefined
6

Je suis curieux de savoir l'ordre d'exécution 1 2 3 7 ... pas les valeurs 'A', 'B' ...

Ma compréhension est que si une promesse est résolue, la fonction "alors" est placée dans la file d'attente d'événements du navigateur. Mon attente était donc 1 2 3 4 ...


@ jfriend00 Merci, merci beaucoup pour les explications détaillées! C'est vraiment une énorme quantité de travail!

29
I.R.

Commentaires

Tout d'abord, l'exécution de promesses à l'intérieur d'un gestionnaire .then() et le non retour de ces promesses à partir du rappel .then() crée une toute nouvelle séquence de promesses non attachée qui n'est en aucun cas synchronisée avec les promesses parent. Habituellement, c'est un bogue et, en fait, certains moteurs de promesse avertissent lorsque vous faites cela car ce n'est presque jamais le comportement souhaité. La seule fois où l'on voudrait faire cela, c'est quand vous faites une sorte de feu et oubliez une opération où vous ne vous souciez pas des erreurs et vous ne vous souciez pas de la synchronisation avec le reste du monde.

Ainsi, toutes vos promesses Promise.resolve() à l'intérieur des gestionnaires .then() créent de nouvelles chaînes Promise qui s'exécutent indépendamment de la chaîne parent. Vous n'avez pas de comportement déterminé. C'est un peu comme lancer quatre appels ajax en parallèle. Vous ne savez pas lequel terminera en premier. Maintenant, étant donné que tout votre code à l'intérieur de ces gestionnaires Promise.resolve() est synchrone (car ce n'est pas du code réel), vous pourriez obtenir un comportement cohérent, mais ce n'est pas le point de promesse de conception, donc je ne le ferais pas 'ne passez pas beaucoup de temps à essayer de déterminer quelle chaîne Promise qui exécute uniquement du code synchrone va finir en premier. Dans le monde réel, cela n'a pas d'importance car si l'ordre est important, vous ne laisserez pas les choses au hasard de cette façon.

Résumé

  1. Tous les gestionnaires .then() sont appelés de manière asynchrone une fois le thread d'exécution en cours terminé (comme le dit la spécification Promises/A +, lorsque le moteur JS revient au "code de plate-forme"). Cela est vrai même pour les promesses résolues de manière synchrone, comme Promise.resolve().then(...). Cela est effectué pour la cohérence de la programmation afin qu'un gestionnaire .then() soit invoqué de manière cohérente de manière asynchrone, que la promesse soit résolue immédiatement ou ultérieurement. Cela évite certains bogues de synchronisation et permet au code appelant de voir l'exécution asynchrone cohérente.

  2. Aucune spécification ne détermine l'ordre relatif des gestionnaires setTimeout() par rapport aux gestionnaires .then() planifiés si les deux sont mis en file d'attente et prêts à être exécutés. Dans votre implémentation, un gestionnaire .then() en attente est toujours exécuté avant un setTimeout() en attente, mais la spécification Promises/A + indique que ce n'est pas déterminé. Il indique que les gestionnaires .then() peuvent être planifiés de nombreuses façons, dont certaines s'exécuteraient avant les appels setTimeout() en attente et d'autres pourraient s'exécuter après les appels setTimeout() en attente. Par exemple, la spécification Promises/A + permet de planifier les gestionnaires .then() avec setImmediate() qui s'exécuterait avant les appels setTimeout() en attente ou avec setTimeout() qui s'exécuterait après des appels setTimeout() en attente. Donc, votre code ne devrait pas du tout dépendre de cet ordre.

  3. Plusieurs chaînes Promise indépendantes n'ont pas d'ordre d'exécution prévisible et vous ne pouvez pas vous fier à un ordre particulier. C'est comme lancer quatre appels ajax en parallèle où vous ne savez pas lequel se terminera en premier.

  4. Si l'ordre d'exécution est important, ne créez pas de course qui dépend de détails de mise en œuvre minutieux. Au lieu de cela, liez les chaînes de promesses pour forcer un ordre d'exécution particulier.

  5. Vous ne souhaitez généralement pas créer de chaînes de promesses indépendantes dans un gestionnaire .then() qui ne sont pas renvoyées par le gestionnaire. Il s'agit généralement d'un bogue, sauf dans de rares cas d'incendie et à oublier sans gestion d'erreur.

Analyse ligne par ligne

Voici donc une analyse de votre code. J'ai ajouté des numéros de ligne et nettoyé l'indentation pour faciliter la discussion:

1     Promise.resolve('A').then(function (a) {
2         console.log(2, a);
3         return 'B';
4     }).then(function (a) {
5         Promise.resolve('C').then(function (a) {
6             console.log(7, a);
7         }).then(function (a) {
8             console.log(8, a);
9         });
10        console.log(3, a);
11        return a;
12    }).then(function (a) {
13        Promise.resolve('D').then(function (a) {
14            console.log(9, a);
15        }).then(function (a) {
16            console.log(10, a);
17        });
18        console.log(4, a);
19    }).then(function (a) {
20        console.log(5, a);
21    });
22   
23    console.log(1);
24    
25    setTimeout(function () {
26        console.log(6)
27    }, 0);

Ligne 1 démarre une chaîne de promesses et y a attaché un gestionnaire .then(). Étant donné que Promise.resolve() se résout immédiatement, la bibliothèque Promise planifiera le premier gestionnaire .then() pour qu'il s'exécute une fois ce thread de Javascript terminé. Dans les bibliothèques de promesses compatibles Promises/A +, tous les gestionnaires .then() sont appelés de manière asynchrone une fois le thread d'exécution en cours terminé et lorsque JS revient à la boucle d'événements. Cela signifie que tout autre code synchrone dans ce fil tel que votre console.log(1) s'exécutera ensuite, ce que vous voyez.

Tous les autres gestionnaires .then() au niveau supérieur ( lignes 4, 12, 19) enchaînent après le premier et ne fonctionneront qu'après que le premier aura son tour. Ils sont essentiellement mis en file d'attente à ce stade.

Étant donné que setTimeout() se trouve également dans ce thread d'exécution initial, il est exécuté et donc un temporisateur est planifié.

C'est la fin de l'exécution synchrone. Maintenant, le moteur JS commence à exécuter des choses planifiées dans la file d'attente d'événements.

Pour autant que je sache, il n'y a aucune garantie qui vient d'abord un gestionnaire setTimeout(fn, 0) ou .then() qui sont tous deux planifiés pour s'exécuter juste après ce thread d'exécution. Les gestionnaires .then() sont considérés comme des "micro-tâches", il n'est donc pas surprenant qu'ils s'exécutent en premier avant le setTimeout(). Mais, si vous avez besoin d'une commande particulière, vous devez écrire du code qui garantit une commande plutôt que de vous fier à ce détail d'implémentation.

Quoi qu'il en soit, le gestionnaire .then() défini sur ligne 1 s'exécute ensuite. Ainsi, vous voyez la sortie 2 "A" De ce console.log(2, a).

Ensuite, puisque le gestionnaire .then() précédent a renvoyé une valeur simple, cette promesse est considérée comme résolue, de sorte que le gestionnaire .then() défini sur ligne 4 s'exécute. Voici où vous créez une autre chaîne de promesses indépendante et introduisez un comportement qui est généralement un bogue.

Ligne 5, crée une nouvelle chaîne Promise. Il résout cette promesse initiale et planifie ensuite l'exécution de deux gestionnaires .then() lorsque le thread d'exécution en cours est terminé. Dans ce thread d'exécution actuel se trouve le console.log(3, a) sur la ligne 10, c'est pourquoi vous voyez cela ensuite. Ensuite, ce thread d'exécution se termine et il retourne au planificateur pour voir quoi exécuter ensuite.

Nous avons maintenant plusieurs gestionnaires .then() dans la file d'attente en attente d'exécution. Il y a celui que nous venons de planifier à la ligne 5 et il y a le suivant dans la chaîne de niveau supérieur à la ligne 12. Si vous l'aviez fait sur ligne 5:

return Promise.resolve.then(...)

alors vous auriez lié ces promesses entre elles et elles seraient coordonnées en séquence. Mais, en ne retournant pas la valeur de la promesse, vous avez commencé une toute nouvelle chaîne de promesses qui n'est pas coordonnée avec la promesse externe de niveau supérieur. Dans votre cas particulier, le planificateur de promesses décide d'exécuter ensuite le gestionnaire .then() plus profondément imbriqué. Je ne sais pas honnêtement si c'est par spécification, par convention ou simplement un détail d'implémentation d'un moteur de promesse par rapport à l'autre. Je dirais que si l'ordre est essentiel pour vous, alors vous devez forcer un ordre en liant les promesses dans un ordre spécifique plutôt que de compter sur qui remporte la course pour courir en premier.

Quoi qu'il en soit, dans votre cas, c'est une course de planification et le moteur que vous exécutez décide d'exécuter le gestionnaire interne .then() défini à la ligne 5 suivante et vous voyez donc le 7 "C" Spécifié sur line 6. Il ne renvoie alors rien, donc la valeur résolue de cette promesse devient undefined.

De retour dans le planificateur, il exécute le gestionnaire .then() sur ligne 12. C'est encore une course entre ce gestionnaire .then() et celui sur ligne 7 qui attend également de s'exécuter. Je ne sais pas pourquoi il choisit l'un sur l'autre ici, sauf pour dire qu'il peut être indéterminé ou varier selon le moteur de promesse car la commande n'est pas spécifiée par le code. Dans tous les cas, le gestionnaire .then() dans ligne 12 démarre. Cela crée à nouveau une nouvelle ligne de chaîne de promesse indépendante ou non synchronisée la précédente. Il planifie à nouveau un gestionnaire .then(), puis vous obtenez le 4 "B" À partir du code synchrone dans ce gestionnaire .then(). Tout le code synchrone est fait dans ce gestionnaire alors maintenant, il revient au planificateur pour la tâche suivante.

De retour dans le planificateur, il décide d'exécuter le gestionnaire .then() sur ligne 7 et vous obtenez 8 undefined. La promesse y est undefined parce que le gestionnaire .then() précédent dans cette chaîne n'a rien retourné, donc sa valeur de retour était undefined, c'est donc la valeur résolue de la chaîne de promesse à ce moment.

À ce stade, la sortie jusqu'à présent est:

1
2 "A"
3 "B"
7 "C"
4 "B"
8 undefined

Encore une fois, tout le code synchrone est fait, il revient donc au planificateur et il décide d'exécuter le gestionnaire .then() défini sur ligne 13. Cela s'exécute et vous obtenez la sortie 9 "D", Puis elle retourne à nouveau au planificateur.

Conformément à la chaîne Promise.resolve() précédemment imbriquée, le programme choisit d'exécuter le prochain gestionnaire externe .then() défini sur ligne 19. Il s'exécute et vous obtenez la sortie 5 undefined. Il s'agit à nouveau de undefined car le gestionnaire .then() précédent dans cette chaîne n'a pas renvoyé de valeur, donc la valeur résolue de la promesse était undefined.

À ce stade, la sortie jusqu'à présent est:

1
2 "A"
3 "B"
7 "C"
4 "B"
8 undefined
9 "D"
5 undefined

À ce stade, il n'y a qu'un seul gestionnaire .then() prévu pour être exécuté, il exécute donc celui défini sur ligne 15 et vous obtenez la sortie 10 undefined Ensuite.

Enfin, enfin, setTimeout() s'exécute et le résultat final est:

1
2 "A"
3 "B"
7 "C"
4 "B"
8 undefined
9 "D"
5 undefined
10 undefined
6

Si l'on essayait de prédire exactement l'ordre dans lequel cela se déroulerait, il y aurait alors deux questions principales.

  1. Comment les gestionnaires .then() sont-ils priorisés par rapport aux appels setTimeout() qui sont également en attente?.

  2. Comment le moteur de promesse décide-t-il de prioriser plusieurs gestionnaires .then() qui attendent tous d'être exécutés. D'après vos résultats avec ce code, ce n'est pas FIFO.

Pour la première question, je ne sais pas si c'est par spécification ou simplement un choix d'implémentation ici dans le moteur de promesse/moteur JS, mais l'implémentation que vous avez signalée semble prioriser tous les gestionnaires .then() en attente avant tout setTimeout() appels. Votre cas est un peu étrange car vous n'avez aucun appel d'API asynchrone réel autre que la spécification de gestionnaires .then(). Si vous aviez une opération asynchrone qui prenait réellement du temps réel à exécuter au début de cette chaîne de promesses, votre setTimeout() s'exécuterait avant le gestionnaire .then() sur la véritable opération asynchrone simplement parce que le réel L'exécution d'une opération asynchrone prend du temps réel. Donc, c'est un peu un exemple artificiel et ce n'est pas le cas de conception habituel pour du code réel.

Pour la deuxième question, j'ai vu une discussion qui explique comment les gestionnaires .then() en attente à différents niveaux d'imbrication doivent être priorisés. Je ne sais pas si cette discussion a jamais été résolue dans une spécification ou non. Je préfère coder de manière à ce que ce niveau de détail ne m'importe pas. Si je me soucie de l'ordre de mes opérations asynchrones, je lie mes chaînes de promesses pour contrôler l'ordre et ce niveau de détail d'implémentation ne m'affecte en aucune façon. Si je ne me soucie pas de la commande, alors je ne me soucie pas de la commande, encore une fois, le niveau de détail de la mise en œuvre ne m'affecte pas. Même si c'était dans certaines spécifications, il semble que le type de détail ne devrait pas être approuvé dans de nombreuses implémentations différentes (différents navigateurs, différents moteurs de promesse) à moins que vous ne l'ayez testé partout où vous alliez exécuter. Donc, je vous recommande de ne pas vous fier à un ordre d'exécution spécifique lorsque vous avez des chaînes de promesses non synchronisées.


Vous pouvez déterminer l'ordre à 100% en liant simplement toutes vos chaînes de promesses comme ceci (en retournant les promesses internes afin qu'elles soient liées dans la chaîne parent):

Promise.resolve('A').then(function (a) {
    console.log(2, a);
    return 'B';
}).then(function (a) {
    var p =  Promise.resolve('C').then(function (a) {
        console.log(7, a);
    }).then(function (a) {
        console.log(8, a);
    });
    console.log(3, a);
    // return this promise to chain to the parent promise
    return p;
}).then(function (a) {
    var p = Promise.resolve('D').then(function (a) {
        console.log(9, a);
    }).then(function (a) {
        console.log(10, a);
    });
    console.log(4, a);
    // return this promise to chain to the parent promise
    return p;
}).then(function (a) {
    console.log(5, a);
});

console.log(1);

setTimeout(function () {
    console.log(6)
}, 0);

Cela donne la sortie suivante dans Chrome:

1
2 "A"
3 "B"
7 "C"
8 undefined
4 undefined
9 "D"
10 undefined
5 undefined
6

Et, puisque la promesse a tous été enchaînés, l'ordre de la promesse est tous définis par le code. La seule chose qui reste comme détail d'implémentation est le timing du setTimeout() qui, comme dans votre exemple, vient en dernier, après tous les gestionnaires .then() en attente.

Modifier:

Après examen de la spécification Promises/A + , nous trouvons ceci:

2.2.4 onFulfilled ou onRejected ne doit pas être appelé tant que la pile de contexte d'exécution ne contient que du code de plate-forme. [3.1].

....

3.1 Ici, "code de plate-forme" désigne le moteur, l'environnement et le code d'implémentation de la promesse. En pratique, cette exigence garantit que onFulfilled et onRejected s'exécutent de manière asynchrone, après le tour de boucle d'événement dans lequel il est ensuite appelé, et avec une nouvelle pile. Cela peut être mis en œuvre avec un mécanisme de "macro-tâche" tel que setTimeout ou setImmediate, ou avec un mécanisme de "micro-tâche" tel que MutationObserver ou process.nextTick. Étant donné que l'implémentation de la promesse est considérée comme du code de plate-forme, elle peut elle-même contenir une file d'attente de planification des tâches ou "trampoline" dans lequel les gestionnaires sont appelés.

Cela signifie que les gestionnaires .then() doivent s'exécuter de manière asynchrone après le retour de la pile d'appels au code de la plate-forme, mais laisse entièrement à l'implémentation comment faire exactement cela, que ce soit avec une macro-tâche comme setTimeout() ou micro-tâche comme process.nextTick(). Ainsi, selon cette spécification, elle n'est pas déterminée et ne doit pas être invoquée.

Je ne trouve aucune information sur les macro-tâches, les micro-tâches ou le calendrier des gestionnaires de promesse .then() par rapport à setTimeout() dans la spécification ES6. Cela n'est peut-être pas surprenant car setTimeout() lui-même ne fait pas partie de la spécification ES6 (c'est une fonction d'environnement Host, pas une fonction de langage).

Je n'ai trouvé aucune spécification pour soutenir cela, mais les réponses à cette question Différence entre microtâche et macrotâche dans un contexte de boucle d'événement expliquer comment les choses ont tendance à fonctionner dans les navigateurs avec des macro-tâches et des micro- Tâches.

Pour info, si vous voulez plus d'informations sur les micro-tâches et macro-tâches, voici un article de référence intéressant sur le sujet: Tâches, microtâches, files d'attente et plannings .

74
jfriend00

Le moteur JavaScript du navigateur a quelque chose appelé la "boucle d'événements". Il n'y a qu'un seul thread de code JavaScript en cours d'exécution à la fois. Lorsqu'un bouton est cliqué ou qu'une demande AJAX ou autre chose asynchrone se termine, un nouvel événement est placé dans la boucle d'événements. Le navigateur exécute ces événements un par un.

Ce que vous regardez ici, c'est que vous exécutez du code qui s'exécute de manière asynchrone. Une fois le code asynchrone terminé, il ajoute un événement approprié à la boucle d'événements. L'ordre dans lequel les événements sont ajoutés dépend de la durée de chaque opération asynchrone.

Cela signifie que si vous utilisez quelque chose comme AJAX où vous n'avez aucun contrôle sur l'ordre des requêtes, vos promesses peuvent s'exécuter dans un ordre différent à chaque fois.

1