Como cancelar uma cadeia de promises EMCAScript6 (vanilla JavaScript)

Existe um método para limpar as .then de uma instância do JavaScript Promise ?

Eu escrevi um framework de teste JavaScript em cima do QUnit . O framework executa testes de forma síncrona, executando cada um em um Promise . (Desculpe pela duração deste bloco de código. Eu o comentei da melhor maneira possível, por isso parece menos tedioso.)

 /* 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 um teste expirar, meu Promessa de tempo limite assert.fail() no teste para que o teste seja marcado como falho, o que é bom, mas o teste continua a ser executado porque o teste Promise ( result ) ainda está aguardando para resolvê-lo.

Eu preciso de uma boa maneira de cancelar meu teste. Eu posso fazer isso criando um campo no módulo framework this.cancelTest ou algo assim, e checando de vez em quando (por exemplo, no começo de cada iteração) dentro do teste se deve cancelar. No entanto, idealmente, eu poderia usar $$(at).on("timeout", /* something here */) para limpar o restante then() s na minha variável de result , para que nenhum do resto do teste seja executado .

Será que algo assim existe?

Rápida atualização

Eu tentei usar Promise.race([result, at.promise]) . Não funcionou.

Atualização 2 + confusão

Para me desbloquear, adicionei algumas linhas com o mod.cancelTest / polling dentro da ideia de teste. (Também removi o acionador de 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); }); 

Eu defino um ponto de interrupção na instrução catch e ele está sendo atingido. O que está me confundindo agora é que a declaração then() não está sendo chamada. Idéias?

Atualização 3

Descobri a última coisa fora. fn.call() estava lançando um erro que eu não peguei, então a promise do teste foi rejeitada antes que at.promise.catch() pudesse resolvê-lo.

Existe um método para limpar as .then de uma instância do JavaScript Promise?

Não. Não no ECMAScript 6 pelo menos. As promises (e seus manipuladores em then ) são incuráveis ​​por padrão (infelizmente) . Há um pouco de discussão sobre es-discuss (por exemplo, aqui ) sobre como fazer isso da maneira certa, mas qualquer que seja a abordagem que vencerá, ela não chegará ao ES6.

O ponto de vista atual é que a subsorting permitirá criar promises canceláveis ​​usando sua própria implementação (não tenho certeza de como isso funcionará) .

Até que o comitê de idiomas tenha descoberto a melhor maneira (esperamos que seja o ES7), você ainda pode usar implementações do Promise do usuário, muitas das quais apresentam cancelamento.

A discussão atual está nos rascunhos https://github.com/domenic/cancelable-promise e https://github.com/bergus/promise-cancellation .

Embora não exista uma maneira padrão de fazer isso no ES6, existe uma biblioteca chamada Bluebird para lidar com isso.

Há também uma maneira recomendada descrita como parte da documentação do reagente. Parece semelhante ao que você tem em suas atualizações 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 

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

Estou muito surpreso que ninguém mencione Promise.race como um candidato para isso:

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

versão simples :

apenas dê a function de rejeição.

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

uma solução de wraper (fábrica)

a solução que encontrei é passar um object cancel_holder. ele terá uma function de cancelamento. se tiver uma function de cancelamento, é cancelável.

Esta function de cancelamento rejeita a promise com Erro (‘cancelado’).

Antes de resolver, rejeitar ou on_cancel, evite que a function de cancelamento seja chamada sem motivo.

Eu achei conveniente para passar a ação de cancelamento por injeção

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

Se você quiser impedir que todas as capturas sejam executadas, você pode fazer isso injetando uma promise que nunca será resolvida. Provavelmente, ele tem reprocessos de memory leaks, mas isso corrigirá o problema e não causará muita perda de memory na maioria dos aplicativos.

 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 

Aqui está nossa implementação https://github.com/permettez-moi-de-construire/cancellable-promise

Usado como

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

qual :

  • Não toca na API Promise
  • Deixe-nos fazer o cancelamento adicional dentro de chamada de catch
  • Confie no cancelamento sendo rejeitado em vez de resolvido ao contrário de qualquer outra proposta ou implementação

Puxa e comenta bem-vindo

Defina uma propriedade “cancelada” na promise para sinalizar then() e catch() para sair mais cedo. É muito eficaz, especialmente em Web Workers que possuem microtarefas existentes enfileiradas em Promises de manipuladores 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; } 

A resposta de @Michael Yagudaev funciona para mim.

Mas a resposta original não encadernou a promise embrulhada com o .catch () para lidar com a rejeição, aqui está a minha melhora em cima da resposta do @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();