Cancelar uma corrente ECMAScript 6 Promise vanilla

110

Existe um método para limpar os .thens de uma Promiseinstância de JavaScript ?

Eu escrevi uma estrutura de teste de JavaScript em cima do QUnit . A estrutura executa testes de forma síncrona, executando cada um em a Promise. (Desculpe pela extensão deste bloco de código. Comentei-o da melhor maneira que pude, 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 atingir o tempo limite, minha promessa de tempo limite será assert.fail()no teste para que o teste seja marcado como com falha, o que é muito bom, mas o teste continua a ser executado porque a promessa de teste ( result) ainda está esperando para resolvê-lo.

Preciso de uma boa maneira de cancelar meu teste. Posso fazer isso criando um campo no módulo do framework this.cancelTestou algo assim, e verificando de vez em quando (por exemplo, no início de cada then()iteração) dentro do teste para cancelar. No entanto, idealmente, eu poderia usar $$(at).on("timeout", /* something here */)para limpar os then()s restantes em minha resultvariável, de modo que nada do resto do teste seja executado.

Será que algo assim existe?

Rápida atualização

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

Atualização 2 + confusão

Para me desbloquear, adicionei algumas linhas com mod.cancelTest/ polling na ideia de teste. (Eu também removi o gatilho 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 defini um ponto de interrupção na catchdeclaração e ele está sendo atingido. O que está me confundindo agora é que a then()declaração não está sendo chamada. Ideias?

Atualização 3

Descobri a última coisa. fn.call()estava gerando um erro que não percebi, então a promessa de teste foi rejeitada antes que at.promise.catch()pudesse resolvê-lo.

dx_over_dt
fonte
É possível fazer o cancelamento com promessas ES6, mas não é uma propriedade da promessa (ao contrário - é uma propriedade da função que a retorna). Posso dar um breve exemplo se você estiver interessado.
Benjamin Gruenbaum
@BenjaminGruenbaum Eu sei que já se passou quase um ano, mas ainda estou interessado se você tiver tempo para escrever um exemplo. :)
dx_over_dt
1
Já se passou um ano, mas foi discutido oficialmente dois dias antes de ontem com tokens de cancelamento e promessas canceláveis ​​passando para o estágio 1.
Benjamin Gruenbaum
3
A resposta ES6 para cancelar uma promessa é observável. Você pode ler mais sobre isso aqui: github.com/Reactive-Extensions/RxJS
Frank Goortani
Vinculando minha resposta sobre o uso da Prexbiblioteca para cancelamento de promessa.
noseratio

Respostas:

75

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

Não. Não no ECMAScript 6, pelo menos. As promessas (e seus thenmanipuladores) não podem ser canceladas por padrão (infelizmente) . Há um pouco de discussão em es-discuss (por exemplo, aqui ) sobre como fazer isso da maneira certa, mas seja qual for a abordagem que vai vencer, não vai cair no ES6.

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

Até que o comitê de linguagem tenha descoberto a melhor maneira (esperançosamente ES7?), Você ainda pode usar as implementações do Promise do userland, muitas das quais com cancelamento de recurso.

Discussão atual está na https://github.com/domenic/cancelable-promise e https://github.com/bergus/promise-cancellation rascunhos.

Bergi
fonte
2
"Um pouco de discussão" - posso criar um link para talvez 30 tópicos no esdiscuss ou no GitHub :) (sem mencionar sua própria ajuda com o cancelamento no bluebird 3.0)
Benjamin Gruenbaum
@BenjaminGruenbaum: Você tem esses links prontos para compartilhar em algum lugar? Há muito tempo quero resumir opiniões e tentativas e postar uma proposta para discussão, então ficaria feliz se pudesse verificar novamente que não esqueci de nada.
Bergi,
Tenho-os à mão no trabalho - por isso, terei deles dentro de 3-4 dias. Você pode verificar as especificações de cancelamento de promessa em promises-aplus para um bom começo.
Benjamin Gruenbaum
1
@ LUH3417: funções "normais" são enfadonhas nesse aspecto. Você inicia um programa e espera até que ele termine - ou você killo ignora em que estado possivelmente estranho os efeitos colaterais deixaram seu ambiente (então você normalmente apenas joga fora isso, por exemplo, quaisquer saídas incompletas). Funções não bloqueadoras ou assíncronas, no entanto, são criadas para funcionar em aplicativos interativos, onde você deseja ter esse tipo de controle mais preciso sobre a execução de operações em andamento.
Bergi
6
Domenic removeu a proposta TC39 ... ... cc @BenjaminGruenbaum
Sergio
50

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

Há também uma forma recomendada descrita como parte da documentação de reação. É semelhante ao que você tem nas suas 2 e 3 atualizações.

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

Michael Yagudaev
fonte
1
esta definição de cancelado é apenas rejeitar a promessa. depende da definição de "cancelado".
Alexander Mills
1
E o que acontece se você quiser cancelar um conjunto de promessas?
Matthieu Brucher
1
O problema com essa abordagem é se você tiver uma promessa que nunca será resolvida ou rejeitada, nunca será cancelada.
DaNeSh
2
Isso está parcialmente certo, mas se você tiver uma longa cadeia de promessas, essa abordagem não funcionará.
Veikko Karsikko
11

Estou realmente surpreso que ninguém menciona Promise.racecomo candidato para isto:

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 });
Pho3nixHun
fonte
3
Eu não acredito que isso funcione. Se você alterar a promessa de log, a execução cancel()ainda resultará na chamada do log. `` `const actualPromise = new Promise ((resolver, rejeitar) => {setTimeout (() => {console.log ('real chamado'); resolve ()}, 10000)}); `` `
shmck
2
A questão era como cancelar uma promessa (=> parar thens encadeados a serem executados), não como cancelar setTimeout(=> clearTimeout) ou código síncrono, onde a menos que você coloque um if após cada linha ( if (canceled) return), isso não pode ser alcançado. (Não faça isso)
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();
Slava M
fonte
5

Na verdade, é impossível impedir a execução da promessa, mas você pode sequestrar a rejeição e chamá-la da própria promessa.

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);
Nikksan
fonte
1
@dx_over_dt Sua edição seria um ótimo comentário, mas não uma edição. Por favor, deixe essas edições substanciais para o controle do OP (a menos que a postagem seja marcada como Community Wiki, é claro).
TylerH
@TylerH então é o ponto de edição para corrigir erros de digitação e similares? Ou para atualizar as informações à medida que ficam desatualizadas? Eu sou novo na habilidade de editar o privilégio de postagens de outras pessoas.
dx_over_dt
@dx_over_dt Sim, editar é para melhorar as postagens corrigindo erros de digitação, erros gramaticais e adicionar realce de sintaxe (se alguém apenas postar um monte de código, mas não indentar ou marcar com `` `, por exemplo). Adicionar conteúdo substantivo, como explicações adicionais ou raciocínio / justificativas para coisas, é normalmente a competência da pessoa que postou a resposta. Você é livre para sugerir nos comentários, e o OP será notificado sobre o comentário e poderá respondê-lo, ou poderá simplesmente incorporar sua sugestão à postagem.
TylerH
@dx_over_dt As exceções são se uma postagem estiver marcada como "Wiki da comunidade", indicando que se destina a servir como uma postagem colaborativa (por exemplo, Wikipedia), ou se houver problemas sérios com a postagem, como linguagem rude / abusiva, conteúdo perigoso / prejudicial ( por exemplo, sugestões ou códigos que podem transmitir a você um vírus ou fazer com que você seja preso, etc.), ou informações pessoais como registros de saúde, números de telefone, cartões de crédito, etc .; sinta-se à vontade para removê-los você mesmo.
TylerH
É importante notar que o motivo pelo qual a execução não pode ser interrompida dentro de uma promessa é que o JavaScript é de thread único. Enquanto a função de promessa está sendo executada, nada mais está sendo executado, portanto, não há nada para desencadear a interrupção da execução.
dx_over_dt
2

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
  • Vamos fazer mais cancelamentos dentro de uma catchchamada
  • Confie no cancelamento sendo rejeitado em vez de resolvido, ao contrário de qualquer outra proposta ou implementação

Puxões e comentários são bem-vindos

Cyril CHAPON
fonte
2

A promessa pode ser cancelada com a ajuda de AbortController.

Existe um método para limpar então: sim, você pode rejeitar a promessa com o AbortControllerobjeto e, em seguida, o promiseignorará todos os bloqueios e irá diretamente para o bloco catch.

Exemplo:

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: precisa adicionar polyfill, não é compatível com todos os navegadores.

Exemplo ao Vivo

Editar elegant-lake-5jnh3

Sohail
fonte
1

versão simples :

apenas forneça a função 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 wraper (fábrica)

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

Esta função de cancelamento rejeita a promessa com Erro ('cancelada').

Antes de resolver, rejeitar ou on_cancel evita que a função cancel seja chamada sem motivo.

Achei conveniente 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')
})
Shimon Doodkin
fonte
1

Tente abortável de promessa : 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();
});
Devi
fonte
1

Se o seu código for colocado em uma classe, você pode usar um decorador para isso. Você tem esse decorador em utils-decorators ( npm install --save utils-decorators). Ele cancelará a chamada anterior do método decorado se antes da resolução da chamada anterior tiver sido feita outra chamada para aquele método específico.

import {cancelPrevious} from 'utils-decorators';

class SomeService {

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

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

vlio20
fonte
0

Se você quiser impedir que todos os thens / catch sejam executados, você pode fazer isso injetando uma promessa que nunca será resolvida. Provavelmente ele tem reorientações de vazamento de memória, mas corrigirá o problema e não deve causar muito desperdício de memória 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
DanLatimer
fonte
0

Defina uma propriedade "cancelada" na promessa de sinalizar then()e catch()sair mais cedo. É muito eficaz, especialmente em Web Workers que têm microtarefas existentes enfileiradas em Promises de onmessagemanipuladores.

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

AnthumChris
fonte
0

A resposta de @Michael Yagudaev funciona para mim.

Mas a resposta original não encadeava a promessa embrulhada com .catch () para lidar com o tratamento de rejeições, aqui está meu aprimoramento em cima da resposta 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();

fonte
0

Se p for uma variável que contém uma promessa, então p.then(empty);deve descartar a promessa quando ela finalmente for concluída ou se já estiver completa (sim, eu sei que esta não é a pergunta original, mas é minha pergunta). "vazio" é function empty() {}. Sou apenas um iniciante e provavelmente estou errado, mas essas outras respostas parecem muito complicadas. As promessas devem ser simples.

David Spector
fonte
0

Ainda estou trabalhando com essa ideia, mas aqui está como implementei uma promessa cancelável usando setTimeout como exemplo.

A ideia é que uma promessa seja resolvida ou rejeitada sempre que você decidir que é, portanto, deve ser uma questão de decidir quando você deseja cancelar, satisfazer o critério e, em seguida, chamar a reject()função você mesmo.

  • Em primeiro lugar, acho que há duas razões para terminar uma promessa mais cedo: terminá-la (o que chamei de resolver ) e cancelar (que chamei de rejeitar ). Claro, isso é apenas meu sentimento. Claro que existe um Promise.resolve()método, mas está no próprio construtor e retorna uma promessa resolvida fictícia. Este resolve()método de instância realmente resolve um objeto de promessa instanciado.

  • Em segundo lugar, você pode facilmente adicionar qualquer coisa que você gosta de um objeto promessa recém-criado antes de devolvê-lo, e por isso tenho apenas acrescentado resolve()e reject()métodos para torná-lo auto-suficiente.

  • Terceiro, o truque é ser capaz de acessar o executor resolvee as rejectfunções mais tarde, então simplesmente armazenei-os em um objeto simples de dentro do encerramento.

Acho que a solução é simples e não consigo ver nenhum grande problema nela.

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>

Manngo
fonte