Annulla una catena ECMAScript 6 Promise vanilla


110

Esiste un metodo per cancellare .theni messaggi di posta elettronica da Promiseun'istanza JavaScript ?

Ho scritto un framework di test JavaScript su QUnit . Il framework esegue i test in modo sincrono eseguendoli ciascuno in un file Promise. (Ci scusiamo per la lunghezza di questo blocco di codice. L'ho commentato come meglio posso, quindi sembra meno noioso.)

/* 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);
        });
    });
}

Se un test scade, la mia Promessa di timeout verrà assert.fail()eseguita sul test in modo che il test sia contrassegnato come non riuscito, il che va bene, ma il test continua a essere eseguito perché il test Promise ( result) è ancora in attesa di risolverlo.

Ho bisogno di un buon modo per annullare il mio test. Posso farlo creando un campo sul modulo del framework this.cancelTesto qualcosa del genere, e controllando ogni tanto (es. All'inizio di ogni then()iterazione) all'interno del test se annullare. Tuttavia, idealmente, potrei usare $$(at).on("timeout", /* something here */)per cancellare i restanti then()s sulla mia resultvariabile, in modo che nessuno dei restanti test venga eseguito.

Esiste qualcosa di simile?

Aggiornamento rapido

Ho provato a usare Promise.race([result, at.promise]). Non ha funzionato.

Aggiorna 2 + confusione

Per sbloccarmi, ho aggiunto alcune righe con mod.cancelTest/ polling all'interno dell'idea di test. (Ho anche rimosso il trigger di evento.)

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);
});

Ho impostato un punto di interruzione nell'istruzione catche viene raggiunto. Quello che mi confonde ora è che la then()dichiarazione non viene chiamata. Idee?

Aggiorna 3

Ho scoperto l'ultima cosa. fn.call()stava lanciando un errore che non avevo rilevato, quindi la promessa del test veniva rifiutata prima di at.promise.catch()poterlo risolvere.


È possibile annullare con le promesse ES6 ma non è una proprietà della promessa (piuttosto - è una proprietà della funzione che la restituisce) Posso fare un breve esempio se sei interessato.
Benjamin Gruenbaum

@BenjaminGruenbaum So che è passato quasi un anno, ma mi interessa ancora se hai tempo per scrivere un esempio. :)
dx_over_dt

1
È passato un anno ma se ne è parlato ufficialmente due giorni prima di ieri con token di cancellazione e promesse cancellabili che si spostano alla fase 1.
Benjamin Gruenbaum

3
La risposta ES6 all'annullamento di una promessa è osservabile. Puoi leggere di più su questo qui: github.com/Reactive-Extensions/RxJS
Frank Goortani

Collegamento della mia risposta sull'utilizzo della Prexlibreria per l'annullamento della promessa.
noseratio

Risposte:


75

Esiste un metodo per cancellare .theni messaggi di posta elettronica da un'istanza JavaScript Promise?

No. Almeno non in ECMAScript 6. Le promesse (ei loro thengestori) non sono cancellabili per impostazione predefinita (sfortunatamente) . C'è un po 'di discussione su es-discuss (ad esempio qui ) su come farlo nel modo giusto, ma qualunque approccio vincerà, non atterrerà in ES6.

Il punto di vista attuale è che la sottoclasse consentirà di creare promesse cancellabili utilizzando la propria implementazione (non sono sicuro di come funzionerà) .

Fino a quando il comitato linguistico non avrà individuato il modo migliore (si spera ES7?) , È ancora possibile utilizzare le implementazioni Promise userland, molte delle quali annullate .

La discussione attuale è nelle bozze https://github.com/domenic/cancelable-promise e https://github.com/bergus/promise-cancellation .


2
"Un po 'di discussione" - Posso collegarmi a forse 30 discussioni su esdiscuss o GitHub :) (per non parlare del tuo aiuto con la cancellazione in bluebird 3.0)
Benjamin Gruenbaum

@BenjaminGruenbaum: hai quei link pronti per essere condivisi da qualche parte? Da tempo desideravo riassumere opinioni e tentativi e pubblicare una proposta di discussione, quindi sarei felice se potessi ricontrollare che non ho dimenticato nulla.
Bergi

Li ho a portata di mano al lavoro, quindi li avrò in 3-4 giorni. Puoi controllare le specifiche di cancellazione della promessa in Promises-aplus per un buon inizio.
Benjamin Gruenbaum

1
@ LUH3417: le funzioni "normali" sono noiose a questo riguardo. Avvii un programma e aspetti finché non è finito - o killlo ignori in quale stato forse strano gli effetti collaterali hanno lasciato il tuo ambiente (quindi in genere butti via anche quello, ad esempio qualsiasi output finito a metà). Tuttavia, le funzioni non bloccanti o asincrone sono progettate per funzionare in applicazioni interattive, dove si desidera avere questo tipo di controllo più preciso sull'esecuzione delle operazioni in corso.
Bergi

6
Domenic ha rimosso la proposta del TC39 ... ... cc @BenjaminGruenbaum
Sergio

50

Sebbene non ci sia un modo standard per farlo in ES6, esiste una libreria chiamata Bluebird per gestirlo.

C'è anche un modo consigliato descritto come parte della documentazione di react. Sembra simile a quello che hai nei tuoi aggiornamenti 2 e 3.

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

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


1
questa definizione di annullato sta solo rifiutando la promessa. dipende dalla definizione di "annullato".
Alexander Mills

1
E cosa succede se vuoi cancellare una serie di promesse?
Matthieu Brucher

1
Il problema con questo approccio è che se hai una promessa che non verrà mai risolta o rifiutata, non verrà mai annullata.
DaNeSh

2
Questo è parzialmente corretto, ma se hai una lunga catena di promesse, questo approccio non funzionerebbe.
Veikko Karsikko

11

Sono davvero sorpreso che nessuno menzioni Promise.racecome candidato per questo:

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
Non credo che funzioni. Se si modifica la promessa di log, l'esecuzione cancel()continuerà a richiamare il log. `` const actualPromise = nuova promessa ((risoluzione, rifiuto) => {setTimeout (() => {console.log ('attuale chiamata'); risoluzione ()}, 10000)}); `` `
shmck

2
La domanda era come annullare una promessa (=> fermare le thens concatenate da eseguire), non come annullare setTimeout(=> clearTimeout) o il codice sincrono, dove a meno che non si inserisca un if dopo ogni riga ( if (canceled) return) questo non può essere ottenuto. (Non farlo)
Pho3nixHun

10
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;
};

Uso:

const cancelablePromise = makeCancelable(myPromise);
// ...
cancelablePromise.cancel();

5

In realtà è impossibile interrompere l'esecuzione della promessa, ma puoi dirottare il rifiuto e chiamarlo dalla promessa stessa.

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

    return cancelablePromise;
  }
}

Uso:

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

p.catch(console.log);

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

1
@dx_over_dt La tua modifica sarebbe un ottimo commento, ma non una modifica. Si prega di lasciare tali modifiche sostanziali alla competenza del PO (a meno che il post non sia contrassegnato come Community Wiki, ovviamente).
TylerH

@TylerH quindi è il punto di modificare per correggere errori di battitura e simili? O per aggiornare le informazioni quando diventano obsolete? Non ho la possibilità di modificare i privilegi dei post di altre persone.
dx_over_dt

@dx_over_dt Sì, la modifica serve a migliorare i post correggendo errori di battitura, errori grammaticali e aggiungendo l'evidenziazione della sintassi (se qualcuno pubblica solo un mucchio di codice ma non lo indenta o lo tagga con `` '' per esempio). L'aggiunta di contenuti sostanziali come spiegazioni aggiuntive o ragionamenti / giustificazioni per le cose è in genere di competenza della persona che ha pubblicato la risposta. Sei libero di suggerirlo nei commenti e OP riceverà una notifica del commento e potrà quindi rispondere, oppure potranno semplicemente incorporare il tuo suggerimento nel post stesso.
TylerH

@dx_over_dt Le eccezioni sono se un post è contrassegnato come "Community Wiki" indicando che è destinato a servire come post di collaborazione (ad esempio come Wikipedia), o se ci sono problemi seri con il post come linguaggio scortese / offensivo, contenuto pericoloso / dannoso ( ad es. suggerimenti o codici che potrebbero trasmetterti un virus o farti arrestare, ecc.) o informazioni personali come cartelle cliniche, numeri di telefono, carte di credito, ecc .; sentiti libero di rimuoverli tu stesso.
TylerH

Vale la pena notare che il motivo per cui l'esecuzione non può essere interrotta all'interno di una promessa è che JavaScript è a thread singolo. Mentre la funzione di promessa viene eseguita, nient'altro è in esecuzione, quindi non c'è nulla per attivare l'arresto dell'esecuzione.
dx_over_dt


2

Ecco la nostra implementazione https://github.com/permettez-moi-de-construire/cancellable-promise

Usato come

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
})

quale :

  • Non tocca l'API Promise
  • Facciamo un'ulteriore cancellazione all'interno catch chiamata
  • Affidati al fatto che la cancellazione venga rifiutata invece che risolta a differenza di qualsiasi altra proposta o implementazione

Tira e commenti sono benvenuti


2

La promessa può essere annullata con l'aiuto di AbortController.

Esiste un metodo per cancellare quindi: sì, puoi rifiutare la promessa con l' AbortControlleroggetto e quindi promiseignorerà tutto, quindi si bloccherà e andrà direttamente al blocco di cattura.

Esempio:

import "abortcontroller-polyfill";

let controller = new window.AbortController();
let signal = controller.signal;
let elem = document.querySelector("#status")

let example = (signal) => {
    return new Promise((resolve, reject) => {
        let timeout = setTimeout(() => {
            elem.textContent = "Promise resolved";
            resolve("resolved")
        }, 2000);

        signal.addEventListener('abort', () => {
            elem.textContent = "Promise rejected";
            clearInterval(timeout);
            reject("Promise aborted")
        });
    });
}

function cancelPromise() {
    controller.abort()
    console.log(controller);
}

example(signal)
    .then(data => {
        console.log(data);
    })
    .catch(error => {
        console.log("Catch: ", error)
    });

document.getElementById('abort-btn').addEventListener('click', cancelPromise);

html


    <button type="button" id="abort-btn" onclick="abort()">Abort</button>
    <div id="status"> </div>

Nota: è necessario aggiungere polyfill, non supportato in tutti i browser.

Esempio dal vivo

Modifica elegant-lake-5jnh3


1

versione semplice :

dare semplicemente la funzione di rifiuto.

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();} 
 })
}

una soluzione wraper (fabbrica)

la soluzione che ho trovato è passare un oggetto cancel_holder. avrà una funzione di annullamento. se ha una funzione di cancellazione, allora è cancellabile.

Questa funzione di annullamento rifiuta la promessa con Errore ("annullata").

Prima di risolvere, rifiutare o on_cancel impedire che la funzione di annullamento venga chiamata senza motivo.

Ho trovato conveniente passare l'azione di annullamento per iniezione

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

Prova promettibile : 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();
});

1

Se il tuo codice è inserito in una classe potresti usare un decoratore per quello. Hai un tale decoratore in utils-decorators ( npm install --save utils-decorators). Annullerà la precedente invocazione del metodo decorato se prima della risoluzione della chiamata precedente è stata effettuata un'altra chiamata per quel metodo specifico.

import {cancelPrevious} from 'utils-decorators';

class SomeService {

   @cancelPrevious()
   doSomeAsync(): Promise<any> {
    ....
   }
}

https://github.com/vlio20/utils-decorators#cancelprevious-method


0

Se vuoi impedire l'esecuzione di tutti i tentativi / catture, puoi farlo iniettando una promessa che non si risolverà mai. Probabilmente ha rielaborazioni di perdite di memoria, ma risolverà il problema e non dovrebbe causare troppa memoria sprecata nella maggior parte delle applicazioni.

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

Imposta una proprietà "cancellata" su Promise per segnalare then()e catch()uscire in anticipo. È molto efficace, soprattutto nei Web Worker che dispongono di microtask esistenti in coda in Promises from onmessagehandlers.

// 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

La risposta di @Michael Yagudaev funziona per me.

Ma la risposta originale non concatenava la promessa incartata con .catch () per gestire la gestione dei rifiuti, ecco il mio miglioramento in cima alla risposta di @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

Se p è una variabile che contiene una promessa, allora p.then(empty);dovrebbe ignorare la promessa quando alla fine si completa o se è già completa (sì, so che questa non è la domanda originale, ma è la mia domanda). "vuoto" è function empty() {}. Sono solo un principiante e probabilmente sbaglio, ma queste altre risposte sembrano troppo complicate. Le promesse dovrebbero essere semplici.


0

Sto ancora elaborando questa idea, ma ecco come ho implementato una Promessa cancellabile utilizzando setTimeout come esempio.

L'idea è che una promessa venga risolta o rifiutata ogni volta che hai deciso che lo sia, quindi dovrebbe essere questione di decidere quando annullare, soddisfare il criterio e quindi chiamare la reject()funzione tu stesso.

  • In primo luogo, penso che ci siano due ragioni per terminare una promessa in anticipo: per farla finita (che ho chiamato risoluzione ) e per annullarla (che ho chiamato rifiuto ). Ovviamente è solo una mia sensazione. Ovviamente esiste un Promise.resolve()metodo, ma è nel costruttore stesso e restituisce una promessa risolta fittizia. Questo resolve()metodo di istanza risolve effettivamente un oggetto promesso istanziato.

  • Secondo, puoi felicemente aggiungere qualsiasi cosa ti piaccia a un oggetto promessa appena creato prima di restituirlo, quindi ho appena aggiunto resolve()e reject()metodi per renderlo autonomo.

  • Terzo, il trucco è poter accedere all'esecutore resolvee alle rejectfunzioni in un secondo momento, quindi le ho semplicemente memorizzate in un semplice oggetto all'interno della chiusura.

Penso che la soluzione sia semplice e non vedo grossi problemi con essa.

function wait(delay) {
  var promise;
  var timeOut;
  var executor={};
  promise=new Promise(function(resolve,reject) {
    console.log(`Started`);
    executor={resolve,reject};  //  Store the resolve and reject methods
    timeOut=setTimeout(function(){
      console.log(`Timed Out`);
      resolve();
    },delay);
  });
  //  Implement your own resolve methods,
  //  then access the stored methods
      promise.reject=function() {
        console.log(`Cancelled`);
        clearTimeout(timeOut);
        executor.reject();
      };
      promise.resolve=function() {
        console.log(`Finished`);
        clearTimeout(timeOut);
        executor.resolve();
      };
  return promise;
}

var promise;
document.querySelector('button#start').onclick=()=>{
  promise=wait(5000);
  promise
  .then(()=>console.log('I have finished'))
  .catch(()=>console.log('or not'));
};
document.querySelector('button#cancel').onclick=()=>{ promise.reject(); }
document.querySelector('button#finish').onclick=()=>{ promise.resolve(); }
<button id="start">Start</button>
<button id="cancel">Cancel</button>
<button id="finish">Finish</button>

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.