web-dev-qa-db-fra.com

Annuler une chaîne ECMAScript 6 Promise Vanilla

Existe-t-il une méthode pour effacer le .thens d'une instance JavaScript Promise?

J'ai écrit un framework de test JavaScript sur QUnit . L'infrastructure exécute les tests de manière synchrone en exécutant chacun d'eux dans une Promise. (Désolé pour la longueur de ce bloc de code. Je l'ai commenté du mieux que je pouvais pour que ce soit moins fastidieux.)

/* Promise extension -- used for easily making an async step with a
       timeout without the Promise knowing anything about the function 
       it's waiting on */
$$.extend(Promise, {
    asyncTimeout: function (timeToLive, errorMessage) {
        var error = new Error(errorMessage || "Operation timed out.");
        var res, // resolve()
            rej, // reject()
            t,   // timeout instance
            rst, // reset timeout function
            p,   // the promise instance
            at;  // the returned asyncTimeout instance

        function createTimeout(reject, tempTtl) {
            return setTimeout(function () {
                // triggers a timeout event on the asyncTimeout object so that,
                // if we want, we can do stuff outside of a .catch() block
                // (may not be needed?)
                $$(at).trigger("timeout");

                reject(error);
            }, tempTtl || timeToLive);
        }

        p = new Promise(function (resolve, reject) {
            if (timeToLive != -1) {
                t = createTimeout(reject);

                // reset function -- allows a one-time timeout different
                //    from the one original specified
                rst = function (tempTtl) {
                    clearTimeout(t);
                    t = createTimeout(reject, tempTtl);
                }
            } else {
                // timeToLive = -1 -- allow this promise to run indefinitely
                // used while debugging
                t = 0;
                rst = function () { return; };
            }

            res = function () {
                clearTimeout(t);
                resolve();
            };

            rej = reject;
        });

        return at = {
            promise: p,
            resolve: res,
            reject: rej,
            reset: rst,
            timeout: t
        };
    }
});

/* framework module members... */

test: function (name, fn, options) {
    var mod = this; // local reference to framework module since promises
                    // run code under the window object

    var defaultOptions = {
        // default max running time is 5 seconds
        timeout: 5000
    }

    options = $$.extend({}, defaultOptions, options);

    // remove timeout when debugging is enabled
    options.timeout = mod.debugging ? -1 : options.timeout;

    // call to QUnit.test()
    test(name, function (assert) {
        // tell QUnit this is an async test so it doesn't run other tests
        // until done() is called
        var done = assert.async();
        return new Promise(function (resolve, reject) {
            console.log("Beginning: " + name);

            var at = Promise.asyncTimeout(options.timeout, "Test timed out.");
            $$(at).one("timeout", function () {
                // assert.fail() is just an extension I made that literally calls
                // assert.ok(false, msg);
                assert.fail("Test timed out");
            });

            // run test function
            var result = fn.call(mod, assert, at.reset);

            // if the test returns a Promise, resolve it before resolving the test promise
            if (result && result.constructor === Promise) {
                // catch unhandled errors thrown by the test so future tests will run
                result.catch(function (error) {
                    var msg = "Unhandled error occurred."
                    if (error) {
                        msg = error.message + "\n" + error.stack;
                    }

                    assert.fail(msg);
                }).then(function () {
                    // resolve the timeout Promise
                    at.resolve();
                    resolve();
                });
            } else {
                // if test does not return a Promise, simply clear the timeout
                // and resolve our test Promise
                at.resolve();
                resolve();
            }
        }).then(function () {
            // tell QUnit that the test is over so that it can clean up and start the next test
            done();
            console.log("Ending: " + name);
        });
    });
}

Si un test expire, ma promesse de temporisation assert.fail() sera testée de sorte que le test soit considéré comme ayant échoué, ce qui est très bien, mais le test continue de s'exécuter car le test Promise (result) attend toujours sa résolution.

J'ai besoin d'un bon moyen d'annuler mon test. Je peux le faire en créant un champ sur le module de structure this.cancelTest ou quelque chose du genre et en vérifiant de temps en temps (par exemple, au début de chaque itération then()) dans le test si le test doit être annulé. Cependant, idéalement, je pourrais utiliser $$(at).on("timeout", /* something here */) pour effacer les then()s restants de ma variable result, de sorte qu'aucun autre test ne soit exécuté.

Est-ce que quelque chose comme ça existe?

Mise à jour rapide

J'ai essayé d'utiliser Promise.race([result, at.promise]). Ça n'a pas marché.

Mise à jour 2 + confusion

Pour me débloquer, j'ai ajouté quelques lignes avec le mod.cancelTest/polling dans l'idée de test. (J'ai également supprimé le déclencheur d'événement.)

return new Promise(function (resolve, reject) {
    console.log("Beginning: " + name);

    var at = Promise.asyncTimeout(options.timeout, "Test timed out.");
    at.promise.catch(function () {
        // end the test if it times out
        mod.cancelTest = true;
        assert.fail("Test timed out");
        resolve();
    });

    // ...

}).then(function () {
    // tell QUnit that the test is over so that it can clean up and start the next test
    done();
    console.log("Ending: " + name);
});

J'ai défini un point d'arrêt dans la déclaration catch et il est touché. Ce qui me trouble maintenant, c'est que l'instruction then() n'est pas appelée. Des idées?

Mise à jour 3

Compris la dernière chose. fn.call() a généré une erreur que je n'ai pas détectée. La promesse de test a donc été rejetée avant que at.promise.catch() ne puisse la résoudre.

75
dx_over_dt

Existe-t-il une méthode pour effacer le .thens d'une instance JavaScript Promise?

Non, pas dans ECMAScript 6 au moins. Les promesses (et leurs gestionnaires then) sont impossibles à annuler par défaut (malheureusement). Il y a un peu de discussion sur es-discussion (par exemple ici ) sur la façon de procéder, mais quelle que soit l'approche choisie, elle n'aboutira pas dans ES6.

Le point de vue actuel est que le sous-classement permettra de créer des promesses annulables en utilisant votre propre implémentation (je ne sais pas dans quelle mesure cela fonctionnera).

Tant que le comité des langues n'aura pas trouvé le meilleur moyen (ES7, espérons-le?), Vous pouvez toujours utiliser les implémentations de l'utilisateur, Promise, dont beaucoup annulent les fonctionnalités.

La discussion en cours se trouve dans les https://github.com/domenic/cancelable-promise et https://github.com/bergus/promise-cancellation drafts.

50
Bergi

Bien qu’il n’existe pas de méthode standard dans ES6, il existe une bibliothèque appelée Bluebird pour gérer cela.

Il existe également une méthode recommandée décrite dans la documentation de réaction. Cela ressemble à ce que vous avez dans vos deuxième et troisième mises à jour.

const makeCancelable = (promise) => {
  let hasCanceled_ = false;

  const wrappedPromise = new Promise((resolve, reject) => {
    promise.then((val) =>
      hasCanceled_ ? reject({isCanceled: true}) : resolve(val)
    );
    promise.catch((error) =>
      hasCanceled_ ? reject({isCanceled: true}) : reject(error)
    );
  });

  return {
    promise: wrappedPromise,
    cancel() {
      hasCanceled_ = true;
    },
  };
};

const cancelablePromise = makeCancelable(
  new Promise(r => component.setState({...}}))
);

cancelablePromise
  .promise
  .then(() => console.log('resolved'))
  .catch((reason) => console.log('isCanceled', reason.isCanceled));

cancelablePromise.cancel(); // Cancel the promise

Extrait de: https://facebook.github.io/react/blog/2015/12/16/ismounted-antipattern.html

42
Michael Yagudaev
const makeCancelable = promise => {
    let rejectFn;

    const wrappedPromise = new Promise((resolve, reject) => {
        rejectFn = reject;

        Promise.resolve(promise)
            .then(resolve)
            .catch(reject);
    });

    wrappedPromise.cancel = () => {
        rejectFn({ canceled: true });
    };

    return wrappedPromise;
};

Usage:

const cancelablePromise = makeCancelable(myPromise);
// ...
cancelablePromise.cancel();
4
Slava M

Je suis vraiment surpris que personne ne mentionne Promise.race comme candidat à ceci:

const actualPromise = new Promise((resolve, reject) => { setTimeout(resolve, 10000) });
let cancel;
const cancelPromise = new Promise((resolve, reject) => {
    cancel = reject.bind(null, { canceled: true })
})

const cancelablePromise = Object.assign(Promise.race([actualPromise, cancelPromise]), { cancel });
3
Pho3nixHun

Il existe quelques bibliothèques npm pour les promesses annulables.

  1. p-cancelablehttps://github.com/sindresorhus/p-cancelable

  2. cancelable-promisehttps://github.com/alkemics/CancelablePromise

1
WebBrother

Il est en fait impossible d'empêcher l'exécution de la promesse, mais vous pouvez détourner le rejet et l'appeler à partir de la promesse elle-même.

class CancelablePromise {
  constructor(executor) {
    let _reject = null;
    const cancelablePromise = new Promise((resolve, reject) => {
      _reject = reject;
      return executor(resolve, reject);
    });
    cancelablePromise.cancel = _reject;

    return cancelablePromise;
  }
}

Usage:

const p = new CancelablePromise((resolve, reject) => {
  setTimeout(() => {
    console.log('resolved!');
    resolve();
  }, 2000);
})

p.catch(console.log);

setTimeout(() => {
  p.cancel(new Error('Fucked up!'));
}, 1000);
1
nikksan

version simple :

il suffit de donner la fonction de rejet.

function Sleep(ms,cancel_holder) {

 return new Promise(function(resolve,reject){
  var done=false; 
  var t=setTimeout(function(){if(done)return;done=true;resolve();}, ms);
  cancel_holder.cancel=function(){if(done)return;done=true;if(t)clearTimeout(t);reject();} 
 })
}

une solution d'emballage (usine)

la solution que j'ai trouvée est de passer un objet cancel_holder. il aura une fonction d'annulation. si elle a une fonction d'annulation, elle est annulable.

Cette fonction d'annulation rejette la promesse avec une erreur ('annulée').

Avant résolution, rejetez ou on_cancel empêche la fonction d'annulation d'être appelée sans raison.

J'ai trouvé pratique de passer l'action d'annulation par injection

function cancelablePromise(cancel_holder,promise_fn,optional_external_cancel) {
  if(!cancel_holder)cancel_holder={};
  return new Promise( function(resolve,reject) {
    var canceled=false;
    var resolve2=function(){ if(canceled) return; canceled=true; delete cancel_holder.cancel; resolve.apply(this,arguments);}
    var reject2=function(){ if(canceled) return; canceled=true; delete cancel_holder.cancel; reject.apply(this,arguments);}
    var on_cancel={}
    cancel_holder.cancel=function(){
      if(canceled) return; canceled=true;

      delete cancel_holder.cancel;
      cancel_holder.canceled=true;

      if(on_cancel.cancel)on_cancel.cancel();
      if(optional_external_cancel)optional_external_cancel();

      reject(new Error('canceled'));
    };

    return promise_fn.call(this,resolve2,reject2,on_cancel);        
  });
}

function Sleep(ms,cancel_holder) {

 return cancelablePromise(cancel_holder,function(resolve,reject,oncacnel){

  var t=setTimeout(resolve, ms);
  oncacnel.cancel=function(){if(t)clearTimeout(t);}     

 })
}


let cancel_holder={};

// meanwhile in another place it can be canceled
setTimeout(function(){  if(cancel_holder.cancel)cancel_holder.cancel(); },500) 

Sleep(1000,cancel_holder).then(function() {
 console.log('sleept well');
}, function(e) {
 if(e.message!=='canceled') throw e;
 console.log('sleep interrupted')
})
1
Shimon Doodkin

Voici notre implémentation https://github.com/permettez-moi-de-construire/cancellable-promise

Utilisé comme

const {
  cancellablePromise,
  CancelToken,
  CancelError
} = require('@permettezmoideconstruire/cancellable-promise')

const cancelToken = new CancelToken()

const initialPromise = SOMETHING_ASYNC()
const wrappedPromise = cancellablePromise(initialPromise, cancelToken)


// Somewhere, cancel the promise...
cancelToken.cancel()


//Then catch it
wrappedPromise
.then((res) => {
  //Actual, usual fulfill
})
.catch((err) => {
  if(err instanceOf CancelError) {
    //Handle cancel error
  }

  //Handle actual, usual error
})

lequel :

  • Ne touche pas l'API Promise
  • Laissez-nous faire une autre annulation à l'intérieur de catch call
  • S'appuyer sur l'annulation étant rejeté au lieu de résolu contrairement à toute autre proposition ou mise en œuvre

Tirages et commentaires bienvenus

0
Cyril CHAPON

Essayez promise-abortable : https://www.npmjs.com/package/promise-abortable

$ npm install promise-abortable
import AbortablePromise from "promise-abortable";

const timeout = new AbortablePromise((resolve, reject, signal) => {
  setTimeout(reject, timeToLive, error);
  signal.onabort = resolve;
});

Promise.resolve(fn()).then(() => {
  timeout.abort();
});
0
Devi

Si vous voulez empêcher l'exécution de tous les thens/captures, vous pouvez le faire en injectant une promesse qui ne sera jamais résolue. Il y a probablement des recentrages avec des fuites de mémoire, mais cela résoudra le problème et ne devrait pas causer trop de gaspillage de mémoire dans la plupart des applications.

new Promise((resolve, reject) => {
    console.log('first chain link executed')
    resolve('daniel');
}).then(name => {
    console.log('second chain link executed')
    if (name === 'daniel') {
        // I don't want to continue the chain, return a new promise
        // that never calls its resolve function
        return new Promise((resolve, reject) => {
            console.log('unresolved promise executed')
        });
    }
}).then(() => console.log('last chain link executed'))

// VM492:2 first chain link executed
// VM492:5 second chain link executed
// VM492:8 unresolved promise executed
0
DanLatimer

La réponse de @Michael Yagudaev fonctionne pour moi.

Mais la réponse initiale n'enchaînait pas la promesse enveloppée avec .catch () pour gérer le traitement des rejets, voici mon amélioration par rapport à la réponse de Michael Yagudaev:

const makeCancelablePromise = promise => {
  let hasCanceled = false;
  const wrappedPromise = new Promise((resolve, reject) => {
    promise
      .then(val => (hasCanceled ? reject({ isCanceled: true }) : resolve(val)))
      .catch(
        error => (hasCanceled ? reject({ isCanceled: true }) : reject(error))
      );
  });

  return {
    promise: wrappedPromise,
    cancel() {
      hasCanceled = true;
    }
  };
};

// Example Usage:
const cancelablePromise = makeCancelable(
  new Promise((rs, rj) => {
    /*do something*/
  })
);
cancelablePromise.promise.then(() => console.log('resolved')).catch(err => {
  if (err.isCanceled) {
    console.log('Wrapped promise canceled');
    return;
  }
  console.log('Promise was not canceled but rejected due to errors: ', err);
});
cancelablePromise.cancel();
0
user10175111

Définissez une propriété "annulée" sur la promesse de signaler à then() et à catch() de quitter plus tôt. C'est très efficace, en particulier dans les Web Workers qui ont déjà mis en file d'attente les microtaches dans Promises de gestionnaires onmessage.

// Queue task to resolve Promise after the end of this script
const promise = new Promise(resolve => setTimeout(resolve))

promise.then(_ => {
  if (promise.canceled) {
    log('Promise cancelled.  Exiting early...');
    return;
  }

  log('No cancelation signaled.  Continue...');
})

promise.canceled = true;

function log(msg) {
  document.body.innerHTML = msg;
}

0
AnthumChris

Si p est une variable contenant une promesse, alors p.then(empty); devrait la rejeter quand elle se termine ou si elle est déjà terminée (oui, je sais que ce n'est pas la question de départ, mais c'est la mienne). "vide" est function empty() {}. Je suis juste un débutant et probablement faux, mais ces autres réponses semblent trop compliquées. Les promesses sont supposées être simples.

0
David Spector