Comment synchroniser une séquence de promesses?

j'ai un tableau d'objets de promesse qui doit être résolu dans la même séquence dans laquelle ils sont listés dans le tableau, i.e. nous ne pouvons pas tenter de résoudre un élément jusqu'à ce que le précédent ait été résolu (comme méthode Promise.all([...])).

et si un élément est rejeté, j'ai besoin de la chaîne à rejeter immédiatement, sans tenter de résoudre l'élément suivant.

Comment puis-je implémenter ceci, ou y a-t-il une implémentation existante pour un tel sequence motif?

function sequence(arr) {
    return new Promise(function (resolve, reject) {
        // try resolving all elements in 'arr',
        // but strictly one after another;
    });
}

EDIT

Les premières réponses suggèrent nous ne pouvons que sequence les résultats de tels éléments de Tableau, pas leur exécution, car il est prédéfini dans un tel exemple.

mais alors comment générer un tableau de promesses de manière à éviter une exécution anticipée?

voici un exemple modifié:

function sequence(nextPromise) {
    // while nextPromise() creates and returns another promise,
    // continue resolving it;
}

Je ne voudrais pas en faire une question séparée, parce que je crois qu'elle fait partie de la même problème.

SOLUTION

quelques réponses ci-dessous et les discussions qui ont suivi se sont un peu égarées, mais la solution finale qui a fait exactement ce que je cherchais a été implémentée dans spex bibliothèque, comme méthode ordre. La méthode peut itérer à travers une séquence de longueur dynamique, et créer des promesses comme l'exige la logique commerciale de votre application.

plus Tard, je l'ai transformée en une bibliothèque partagée pour tout le monde à utiliser.

42
demandé sur vitaly-t 2015-04-26 20:07:39

5 réponses

voici quelques exemples simples pour la séquence à travers un tableau qui exécute chaque opération asynchrone en série (l'une après l'autre).

supposons que vous avez un tableau d'éléments:

var arr = [...];

et, vous voulez effectuer une opération asynchrone spécifique sur chaque élément du tableau, une à la fois en série de sorte que la prochaine opération ne commence pas avant que la précédente ne soit terminée.

et, supposons que vous ayez une fonction de retour de promesse pour le traitement de l'un des éléments du tableau:

Itération Manuelle

function processItem(item) {
    // do async operation and process the result
    // return a promise
}

Ensuite, vous pouvez faire quelque chose comme ceci:

function processArray(array, fn) {
    var index = 0;

    function next() {
        if (index < array.length) {
            fn(array[index++]).then(next);
        }
    }
    next();
}

processArray(arr, processItem);

Itération Manuelle Retour De La Promesse

si vous vouliez qu'une promesse vous revienne de processArray() pour que vous sachiez quand il a été fait, vous pouvez ajouter ceci à elle:

function processArray(array, fn) {
    var index = 0;

    return new Promise(function(resolve, reject) {

        function next() {
            if (index < array.length) {
                fn(array[index++]).then(next, reject);
            } else {
                resolve();
            }
        }
        next();
    }
}

processArray(arr, processItem).then(function() {
    // all done here
}, function(reason) {
    // rejection happened
});

Note: cela stoppera la chaîne sur le premier rejet et passera cette raison de nouveau au processArray retourné promettre.

Itération .réduire()

Si vous vouliez faire plus de travail avec des promesses, vous pourriez chaîne de toutes les promesses:

function processArray(array, fn) {
   return array.reduce(function(p, item) {
       return p.then(function() {
          return fn(item);
       });
   }, Promise.resolve());
}

processArray(arr, processItem).then(function(result) {
    // all done here
}, function(reason) {
    // rejection happened
});

Note: Ceci arrêtera la chaîne sur le premier rejet et ramènera cette raison à la promesse retournée de processArray().

pour un scénario de succès, la promesse revient de processArray() sera résolu avec la dernière résolu valeur de votre fn rappel. Si vous voulez accumuler une liste de résultats et résoudre avec cela, vous pouvez collecter les résultats dans un tableau de fermeture de fn et continuer à retourner ce tableau à chaque fois donc la résolution finale serait un tableau de résultats.

Itération .reduce () qui se résout avec Array

Et, depuis, il semble maintenant évident que vous voulez la dernière promesse résultat à un tableau de données (dans l'ordre), voici une révision de la solution précédente qui produit:

function processArray(array, fn) {
   var results = [];
   return array.reduce(function(p, item) {
       return p.then(function() {
           return fn(item).then(function(data) {
               results.push(data);
               return results;
           });
       });
   }, Promise.resolve());
}

processArray(arr, processItem).then(function(result) {
    // all done here
    // array of data here in result
}, function(reason) {
    // rejection happened
});

démo de travail: http://jsfiddle.net/jfriend00/h3zaw8u8/

Et un travail de démonstration qui montre un rejet: http://jsfiddle.net/jfriend00/p0ffbpoc/

Itération .réduire() qui se Résout Avec Tableau avec retard

Et, si vous souhaitez insérer un petit délai entre les opérations:

function delay(t, v) {
    return new Promise(function(resolve) {
        setTimeout(resolve.bind(null, v), t);
    });
}

function processArrayWithDelay(array, t, fn) {
   var results = [];
   return array.reduce(function(p, item) {
       return p.then(function() {
           return fn(item).then(function(data) {
               results.push(data);
               return delay(t, results);
           });
       });
   }, Promise.resolve());
}

processArray(arr, 200, processItem).then(function(result) {
    // all done here
    // array of data here in result
}, function(reason) {
    // rejection happened
});

itération avec la bibliothèque de promesses Bluebird

la bibliothèque de promesses Bluebird a beaucoup de contrôle de la concurrence les fonctionnalités intégrées. Par exemple, pour séquencer une itération à travers un tableau, vous pouvez utiliser Promise.mapSeries().

Promise.mapSeries(arr, function(item) {
    // process each individual item here, return a promise
    return processItem(item);
}).then(function(results) {
    // process final results here
}).catch(function(err) {
    // process array here
});

ou pour insérer un délai entre les itérations:

Promise.mapSeries(arr, function(item) {
    // process each individual item here, return a promise
    return processItem(item).delay(100);
}).then(function(results) {
    // process final results here
}).catch(function(err) {
    // process array here
});

Using ES7 async / wait

si vous codez dans un environnement qui supporte async / wait, vous pouvez aussi utiliser unfor boucle, puis await une promesse dans la boucle et il causera le for boucle pour s'arrêter jusqu'à ce qu'une promesse soit résolue avant de continuer. Ce sera séquencez efficacement vos opérations asynchrones pour que la prochaine ne commence pas avant que la précédente ne soit terminée.

async function processArray(array, fn) {
    let results = [];
    for (let i = 0; i < array.length; i++) {
        let r = await fn(array[i]);
        results.push(r);
    }
    return results;    // will be resolved value of promise
}

// sample usage
processArray(arr, processItem).then(function(result) {
    // all done here
    // array of data here in result
}, function(reason) {
    // rejection happened
});

pour info, je pense que mon processArray() fonction ici est très similaire à Promise.map() dans la bibliothèque de promesses Bluebird qui prend un tableau et une fonction de production de promesse et renvoie une promesse qui se résout avec un tableau de résultats résolus.


@vitaly - t-voici quelques commentaires plus détaillés sur votre approche. Vous pouvez utiliser le code qui vous semble le plus approprié. Quand j'ai commencé à utiliser des promesses, j'ai eu tendance à utiliser les promesses uniquement pour les choses les plus simples qu'ils ont faites et écrire beaucoup de la logique moi-même quand un usage plus avancé des promesses pourrait faire beaucoup plus de lui pour moi. Vous utilisez seulement ce que vous êtes pleinement à l'aise avec et au-delà de cela, vous préférez voir votre propre code que vous connaissez intimement. C'est probablement la nature humaine.

Je suggérerai que comme j'ai compris de plus en plus de ce que les promesses peuvent faire pour moi, j'aime maintenant écrire du code qui utilise plus de fonctionnalités avancées des promesses et il me semble parfaitement naturel et je me sens comme je construis sur une infrastructure bien testée qui a beaucoup de fonctionnalités utiles. Je vous demande seulement de garder l'esprit ouvert alors que vous apprenez de plus en plus à potentiellement aller dans cette direction. Je suis d'avis que c'est une orientation utile et productive de migrer au fur et à mesure que votre compréhension s'améliore.

voici quelques points spécifiques de commentaires sur votre approche:

Vous créez des promesses dans sept lieux

comme contraste dans les styles, mon code n'a que deux les endroits où j'ai choisi de créer une nouvelle promesse - une fois dans l'usine de la fonction et une fois pour initialiser le .reduce() boucle. Partout ailleurs, je ne fais que construire sur les promesses déjà créées en les enchaînant ou en leur rendant des valeurs ou en les rendant directement. Votre code a sept des endroits uniques où vous créez une promesse. Maintenant, un bon codage n'est pas un concours pour voir comment peu d'endroits vous pouvez créer une promesse, mais cela pourrait indiquer la différence dans l'effet de levier les promesses qui sont déjà créées par rapport aux conditions de test et de création de nouvelles promesses.

Jetez-la sécurité est une fonctionnalité très utile

les promesses sont sans danger. Cela signifie qu'une exception au sein d'une promesse gestionnaire rejeter automatiquement cette promesse. Si vous voulez juste que l'exception devienne un rejet, alors c'est une fonctionnalité très utile pour en profiter. En fait, vous constaterez que se jeter soi-même est un moyen utile de rejeter de l'intérieur d'un handler sans créer encore une autre promesse.

Beaucoup Promise.resolve() ou Promise.reject() est probablement une opportunité de simplification

si vous voyez du code avec beaucoup de Promise.resolve() ou Promise.reject() déclarations, alors il y a probablement des occasions de mieux tirer parti des promesses existantes plutôt que de créer toutes ces nouvelles promesses.

Distribution à une Promesse

si vous ne savez pas si quelque chose a retourné une promesse, alors vous pouvez la lancer à une promesse. La bibliothèque promise vérifiera alors elle-même si c'est une promesse ou non et même si c'est le genre de promesse qui correspond à la bibliothèque promise que vous utilisez et, si ce n'est pas le cas, l'enveloppera dans une. Cela peut sauver réécrire un beaucoup de cette logique de vous-même.

Contrat de Retour d'une Promesse

dans de nombreux cas de nos jours, il est tout à fait viable d'avoir un contrat pour une fonction qui peut faire quelque chose d'asynchrone pour rendre une promesse. Si la fonction veut juste faire quelque chose de synchrone, alors elle peut simplement retourner une promesse résolue. Vous semblez vous sentir comme ceci est onéreux, mais c'est certainement la façon dont le vent souffle et j'écris déjà beaucoup de code qui exige que et il se sent très naturel une fois que vous vous familiariser avec les promesses. Il abstraction de savoir si l'opération de synchronisation ou asynchrone, et l'appelant n'a pas à savoir ou à faire quelque chose de spécial. C'est une belle utilisation de promesses.

la fonction factory peut être écrite pour créer une seule promesse

la fonction factory peut être écrite pour créer une seule promesse puis la résoudre ou la rejeter. Ce style rend également jeter sûr de sorte que toute exception produite dans la fonction d'usine devient automatiquement un rejet. Il fait également le contrat de toujours retourner une promesse automatique.

alors que je réalise que cette fonction d'usine est une fonction de mise en place (elle ne fait même rien async), j'espère que vous pouvez voir le style pour le considérer:

function factory(idx) {
    // create the promise this way gives you automatic throw-safety
    return new Promise(function(resolve, reject) {
        switch (idx) {
            case 0:
                resolve("one");
                break;
            case 1:
                resolve("two");
                break;
            case 2:
                resolve("three");
                break;
            default:
                resolve(null);
                break;
        }
    });
}

si l'une de ces opérations était async, alors ils pourraient simplement retourner leurs propres promesses qui seraient automatiquement enchaînées à la seule promesse centrale comme ceci:

function factory(idx) {
    // create the promise this way gives you automatic throw-safety
    return new Promise(function(resolve, reject) {
        switch (idx) {
            case 0:
                resolve($.ajax(...));
            case 1:
                resole($.ajax(...));
            case 2:
                resolve("two");
                break;
            default:
                resolve(null);
                break;
        }
    });
}

utiliser un gestionnaire de rejet pourreturn promise.reject(reason) n'est pas nécessaire

Quand vous avez ce corps de code:

    return obj.then(function (data) {
        result.push(data);
        return loop(++idx, result);
    }, function (reason) {
        return promise.reject(reason);
    });

le gestionnaire de rejets n'ajoute aucune valeur. Vous pouvez au lieu de simplement faire ceci:

    return obj.then(function (data) {
        result.push(data);
        return loop(++idx, result);
    });

Vous êtes déjà en retournant le résultat de obj.then(). Si obj rejette ou si quelque chose est enchaîné à obj ou retourné à partir de, puis .then() gestionnaire rejette, puis obj le rejettera. Si vous pas besoin de créer une nouvelle promesse avec le rejet. Le code plus simple sans le gestionnaire de rejets fait la même chose avec moins de code.


Voici une version dans l'architecture générale de votre code qui tente d'intégrer la plupart de ces idées:

function factory(idx) {
    // create the promise this way gives you automatic throw-safety
    return new Promise(function(resolve, reject) {
        switch (idx) {
            case 0:
                resolve("zero");
                break;
            case 1:
                resolve("one");
                break;
            case 2:
                resolve("two");
                break;
            default:
                // stop further processing
                resolve(null);
                break;
        }
    });
}


// Sequentially resolves dynamic promises returned by a factory;
function sequence(factory) {
    function loop(idx, result) {
        return Promise.resolve(factory(idx)).then(function(val) {
            // if resolved value is not null, then store result and keep going
            if (val !== null) {
                result.push(val);
                // return promise from next call to loop() which will automatically chain
                return loop(++idx, result);
            } else {
                // if we got null, then we're done so return results
                return result;
            }
        });
    }
    return loop(0, []);
}

sequence(factory).then(function(results) {
    log("results: ", results);
}, function(reason) {
    log("rejected: ", reason);
});

démo de travail:http://jsfiddle.net/jfriend00/h3zaw8u8/

Quelques commentaires à propos de cette oeuvre:

  1. Promise.resolve(factory(idx)) essentiellement convertit le résultat de factory(idx) pour une promesse. Si c'était juste une valeur, alors cela devient une promesse résolue avec cette valeur de retour comme valeur de résolution. Si c'était déjà une promesse, elle est liée à cette promesse. Ainsi, il remplace tout votre code de vérification de type sur la valeur de retour du factory() fonction.

  2. la fonction factory indique que c'est fait en retournant null ou une promesse qui est de la valeur résolue finit par être null. Le ci-dessus en fonte établit la correspondance entre ces deux conditions et le même code résultant.

  3. la fonction factory saisit automatiquement les exceptions et les transforme en rejets qui sont alors traités automatiquement par sequence() fonction. C'est un avantage significatif de laisser des promesses faire beaucoup de votre manipulation d'erreur si vous voulez juste avorter le traitement et alimenter l'erreur en retour sur la première exception ou le rejet.

  4. la fonction d'usine dans ce la mise en œuvre peut retourner soit une promesse, soit une valeur statique (pour une opération synchrone) et elle fonctionnera très bien (selon votre demande de conception).

  5. Je l'ai testé avec une exception lancée dans la fonction de rappel de promesse dans la fonction d'usine et il ne fait que rejeter et propager cette exception en arrière pour rejeter la promesse de séquence avec l'exception comme raison.

  6. il utilise une méthode similaire à vous (sur le but, en essayant de restez avec votre architecture générale) pour enchaîner plusieurs appels à loop().

91
répondu jfriend00 2018-01-11 03:34:59

Les promesses représentent valeurs des opérations et non des opérations elles-mêmes. Les opérations sont déjà commencé donc vous ne pouvez pas les faire attendre les uns les autres.

A la place, vous pouvez synchroniser les fonctions qui retournent des promesses les invoquer dans l'ordre (par une boucle avec un chaînage de promesse par exemple), ou en utilisant le .each méthode de bluebird.

9
répondu Benjamin Gruenbaum 2015-04-26 17:36:48

vous ne pouvez pas simplement exécuter des opérations x async et ensuite vouloir qu'elles soient résolues dans un ordre.

la bonne façon de faire quelque chose comme ceci est d'exécuter la nouvelle opération asynchrone seulement après que la précédente a été résolue:

doSomethingAsync().then(function(){
   doSomethingAsync2().then(function(){
       doSomethingAsync3();
       .......
   });
});

Modifier

semble comme vous voulez attendre toutes les promesses et puis invoquer leurs callbacks dans un ordre spécifique. Quelque chose comme ceci:

var callbackArr = [];
var promiseArr = [];
promiseArr.push(doSomethingAsync());
callbackArr.push(doSomethingAsyncCallback);
promiseArr.push(doSomethingAsync1());
callbackArr.push(doSomethingAsync1Callback);
.........
promiseArr.push(doSomethingAsyncN());
callbackArr.push(doSomethingAsyncNCallback);

puis:

$.when(promiseArr).done(function(promise){
    while(callbackArr.length > 0)
    {
       callbackArr.pop()(promise);
    }
});

Les problèmes que peut se produire, c'est qu'une ou plusieurs promesses d'échouer.

5
répondu Amir Popovich 2015-04-26 18:12:15

bien que très dense, voici une autre solution qui itérera une fonction de retour de promesse sur un tableau de valeurs et de résoudre avec un tableau de résultats:

function processArray(arr, fn) {
    return arr.reduce(
        (p, v) => p.then((a) => fn(v).then(r => a.concat([r]))),
        Promise.resolve([])
    );
}

Utilisation:

const numbers = [0, 4, 20, 100];
const multiplyBy3 = (x) => new Promise(res => res(x * 3));

// Prints [ 0, 12, 60, 300 ]
processArray(numbers, multiplyBy3).then(console.log);

notez que, parce que nous réduisons d'une promesse à l'autre, chaque article est traité en série.

C'est fonctionnellement équivalent à la "Itération .reduce () qui résout avec la solution Array" de @jfriend00 mais un peu plus net.

2
répondu Molomby 2017-08-01 04:35:53

je suppose que deux approches pour traiter cette question:

  1. créer plusieurs promesses et utiliser la fonction allWithAsync comme suit:
let allPromiseAsync = (...PromisesList) => {
return new Promise(async resolve => {
    let output = []
    for (let promise of PromisesList) {
        output.push(await promise.then(async resolvedData => await resolvedData))
        if (output.length === PromisesList.length) resolve(output)
    }
}) }
const prm1= Promise.resolve('first');
const prm2= new Promise((resolve, reject) => setTimeout(resolve, 2000, 'second'));
const prm3= Promise.resolve('third');

allPromiseAsync(prm1, prm2, prm3)
    .then(resolvedData => {
        console.log(resolvedData) // ['first', 'second', 'third']
    });
  1. utilisez la promesse.toutes les fonctions à la place:
  (async () => {
  const promise1 = new Promise(resolve => {
    setTimeout(() => { resolve() }, 2500)
  })

  const promise2 = new Promise(resolve => {
    setTimeout(() => { resolve() }, 5000)
  })

  const promise3 = new Promise(resolve => {
    setTimeout(() => { resolve() }, 1000)
  })

  const promises = [promise1, promise2, promise3]

  await Promise.all(promises)

  console.log('This line is shown after 8500ms')
})()
0
répondu Iman Bahrampour 2018-09-03 16:05:55