Eu uso o ES6 Promises para gerenciar toda a minha recuperação de dados de rede e há algumas situações em que preciso forçar o cancelamento.
Basicamente, o cenário é tal que tenho uma pesquisa de digitação antecipada na IU em que a solicitação é delegada ao back-end para realizar a pesquisa com base na entrada parcial. Embora esta solicitação de rede (# 1) possa demorar um pouco, o usuário continua a digitar, o que eventualmente aciona outra chamada de back-end (# 2)
Aqui, o nº 2 naturalmente tem precedência sobre o nº 1, então eu gostaria de cancelar a solicitação de envolvimento da promessa nº 1. Já tenho um cache de todas as Promessas na camada de dados, então posso, teoricamente, recuperá-lo, pois estou tentando enviar uma Promessa para o # 2.
Mas como cancelo a promessa nº 1 depois de recuperá-la do cache?
Alguém poderia sugerir uma abordagem?
fonte
Respostas:
Não. Não podemos fazer isso ainda.
ES6 promessas não suportam o cancelamento ainda . Está a caminho, e seu design é algo em que muitas pessoas trabalharam muito. A semântica de cancelamento de som é difícil de acertar e isso é um trabalho em andamento. Existem debates interessantes sobre o repositório "fetch", no esdiscuss e em vários outros repositórios no GH, mas eu seria paciente se fosse você.
Mas, mas, mas .. o cancelamento é muito importante!
É, a realidade da questão é que o cancelamento é realmente um cenário importante na programação do lado do cliente. Os casos que você descreve, como abortar solicitações da web, são importantes e estão em toda parte.
Então ... a linguagem me ferrou!
Sim, desculpe por isso. As promessas tinham que ser feitas antes que outras coisas fossem especificadas - então elas entraram sem algumas coisas úteis como
.finally
e.cancel
- está a caminho, porém, para a especificação por meio do DOM. O cancelamento não é uma reflexão tardia, é apenas uma restrição de tempo e uma abordagem mais iterativa para o design da API.Então o que eu posso fazer?
Você tem várias alternativas:
Usar uma biblioteca de terceiros é bastante óbvio. Quanto a um token, você pode fazer seu método assumir uma função e chamá-la, como tal:
function getWithCancel(url, token) { // the token is for cancellation var xhr = new XMLHttpRequest; xhr.open("GET", url); return new Promise(function(resolve, reject) { xhr.onload = function() { resolve(xhr.responseText); }); token.cancel = function() { // SPECIFY CANCELLATION xhr.abort(); // abort request reject(new Error("Cancelled")); // reject the promise }; xhr.onerror = reject; }); };
O que o deixaria fazer:
var token = {}; var promise = getWithCancel("/someUrl", token); // later we want to abort the promise: token.cancel();
Seu caso de uso real -
last
Isso não é muito difícil com a abordagem de token:
function last(fn) { var lastToken = { cancel: function(){} }; // start with no op return function() { lastToken.cancel(); var args = Array.prototype.slice.call(arguments); args.push(lastToken); return fn.apply(this, args); }; }
O que o deixaria fazer:
var synced = last(getWithCancel); synced("/url1?q=a"); // this will get canceled synced("/url1?q=ab"); // this will get canceled too synced("/url1?q=abc"); // this will get canceled too synced("/url1?q=abcd").then(function() { // only this will run });
E não, bibliotecas como Bacon e Rx não "brilham" aqui porque são bibliotecas observáveis, elas apenas têm a mesma vantagem que bibliotecas de promessa em nível de usuário têm por não serem vinculadas às especificações. Acho que vamos esperar para ver no ES2016 quando os observáveis se tornarem nativos. Eles são bacanas para digitação antecipada.
fonte
As propostas padrão para promessas canceláveis falharam.
Uma promessa não é uma superfície de controle para a ação assíncrona que a cumpre; confunde proprietário com consumidor. Em vez disso, crie funções assíncronas que podem ser canceladas por meio de algum token passado.
Outra promessa torna um token perfeito, tornando o cancelamento fácil de implementar com
Promise.race
:Exemplo: use
Promise.race
para cancelar o efeito de uma cadeia anterior:let cancel = () => {}; input.oninput = function(ev) { let term = ev.target.value; console.log(`searching for "${term}"`); cancel(); let p = new Promise(resolve => cancel = resolve); Promise.race([p, getSearchResults(term)]).then(results => { if (results) { console.log(`results for "${term}"`,results); } }); } function getSearchResults(term) { return new Promise(resolve => { let timeout = 100 + Math.floor(Math.random() * 1900); setTimeout(() => resolve([term.toLowerCase(), term.toUpperCase()]), timeout); }); }
Search: <input id="input">
Aqui, estamos "cancelando" pesquisas anteriores injetando um
undefined
resultado e testando-o, mas poderíamos facilmente imaginar a rejeição com"CancelledError"
.Claro que isso não cancela a pesquisa de rede, mas isso é uma limitação de
fetch
. Sefetch
tomar uma promessa de cancelamento como argumento, poderá cancelar a atividade de rede.Eu já propôs este "Cancelar padrão promessa" na es-discuss, exatamente para sugerir que
fetch
fazer isso.fonte
Eu verifiquei a referência do Mozilla JS e encontrei isto:
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/race
Vamos dar uma olhada:
var p1 = new Promise(function(resolve, reject) { setTimeout(resolve, 500, "one"); }); var p2 = new Promise(function(resolve, reject) { setTimeout(resolve, 100, "two"); }); Promise.race([p1, p2]).then(function(value) { console.log(value); // "two" // Both resolve, but p2 is faster });
Temos aqui p1 e p2 colocados
Promise.race(...)
como argumentos, isso na verdade está criando uma nova promessa de resolução, que é o que você precisa.fonte
Para Node.js e Electron, eu recomendo fortemente o uso de Promise Extensions for JavaScript (Prex) . Seu autor, Ron Buckton, é um dos principais engenheiros do TypeScript e também o responsável pela atual proposta de cancelamento do ECMAScript do TC39 . A biblioteca está bem documentada e é provável que alguns dos Prex cumpram o padrão.
Em uma nota pessoal e vindo do background do C #, eu gosto muito do fato de que o Prex é modelado sobre a estrutura existente de Cancelamento em Threads Gerenciados , ou seja, com base na abordagem adotada com APIs
CancellationTokenSource
/CancellationToken
.NET. Na minha experiência, eles têm sido muito úteis para implementar uma lógica de cancelamento robusta em aplicativos gerenciados.Eu também verifiquei que ele funciona dentro de um navegador agrupando o Prex usando o Browserify .
Aqui está um exemplo de atraso com cancelamento ( Gist e RunKit , usando Prex para seu
CancellationToken
eDeferred
):// by @noseratio // https://gist.github.com/noseratio/141a2df292b108ec4c147db4530379d2 // https://runkit.com/noseratio/cancellablepromise const prex = require('prex'); /** * A cancellable promise. * @extends Promise */ class CancellablePromise extends Promise { static get [Symbol.species]() { // tinyurl.com/promise-constructor return Promise; } constructor(executor, token) { const withCancellation = async () => { // create a new linked token source const linkedSource = new prex.CancellationTokenSource(token? [token]: []); try { const linkedToken = linkedSource.token; const deferred = new prex.Deferred(); linkedToken.register(() => deferred.reject(new prex.CancelError())); executor({ resolve: value => deferred.resolve(value), reject: error => deferred.reject(error), token: linkedToken }); await deferred.promise; } finally { // this will also free all linkedToken registrations, // so the executor doesn't have to worry about it linkedSource.close(); } }; super((resolve, reject) => withCancellation().then(resolve, reject)); } } /** * A cancellable delay. * @extends Promise */ class Delay extends CancellablePromise { static get [Symbol.species]() { return Promise; } constructor(delayMs, token) { super(r => { const id = setTimeout(r.resolve, delayMs); r.token.register(() => clearTimeout(id)); }, token); } } // main async function main() { const tokenSource = new prex.CancellationTokenSource(); const token = tokenSource.token; setTimeout(() => tokenSource.cancel(), 2000); // cancel after 2000ms let delay = 1000; console.log(`delaying by ${delay}ms`); await new Delay(delay, token); console.log("successfully delayed."); // we should reach here delay = 2000; console.log(`delaying by ${delay}ms`); await new Delay(delay, token); console.log("successfully delayed."); // we should not reach here } main().catch(error => console.error(`Error caught, ${error}`));
Observe que o cancelamento é uma corrida. Ou seja, uma promessa pode ter sido resolvida com sucesso, mas no momento em que você a cumprir (com
await
outhen
), o cancelamento também pode ter sido acionado. Depende de você como vai enfrentar essa corrida, mas não custa pedirtoken.throwIfCancellationRequested()
uma prorrogação, como fiz acima.fonte
Eu enfrentei problema semelhante recentemente.
Eu tinha um cliente baseado em promessa (não em rede) e queria sempre fornecer os dados mais recentes solicitados ao usuário para manter a interface do usuário tranquila.
Depois de lutar com ideia de cancelamento,
Promise.race(...)
ePromise.all(..)
eu só comecei a lembrar o meu último ID de pedido e quando promessa foi cumprida i só foi tornando meus dados quando se combinava com o id de um último pedido.Espero que ajude alguém.
fonte
Veja https://www.npmjs.com/package/promise-abortable
fonte
Você pode rejeitar a promessa antes de terminar:
// Our function to cancel promises receives a promise and return the same one and a cancel function const cancellablePromise = (promiseToCancel) => { let cancel const promise = new Promise((resolve, reject) => { cancel = reject promiseToCancel .then(resolve) .catch(reject) }) return {promise, cancel} } // A simple promise to exeute a function with a delay const waitAndExecute = (time, functionToExecute) => new Promise((resolve, reject) => { timeInMs = time * 1000 setTimeout(()=>{ console.log(`Waited ${time} secs`) resolve(functionToExecute()) }, timeInMs) }) // The promise that we will cancel const fetchURL = () => fetch('https://pokeapi.co/api/v2/pokemon/ditto/') // Create a function that resolve in 1 seconds. (We will cancel it in 0.5 secs) const {promise, cancel} = cancellablePromise(waitAndExecute(1, fetchURL)) promise .then((res) => { console.log('then', res) // This will executed in 1 second }) .catch(() => { console.log('catch') // We will force the promise reject in 0.5 seconds }) waitAndExecute(0.5, cancel) // Cancel previous promise in 0.5 seconds, so it will be rejected before finishing. Commenting this line will make the promise resolve
Infelizmente, a chamada de busca já foi realizada, então você verá a resolução da chamada na guia Rede. Seu código simplesmente irá ignorá-lo.
fonte
Usando a subclasse Promise fornecida pelo pacote externo, isso pode ser feito da seguinte maneira: Demonstração ao vivo
import CPromise from "c-promise2"; function fetchWithTimeout(url, {timeout, ...fetchOptions}= {}) { return new CPromise((resolve, reject, {signal}) => { fetch(url, {...fetchOptions, signal}).then(resolve, reject) }, timeout) } const chain= fetchWithTimeout('http://localhost/') .then(response => response.json()) .then(console.log, console.warn); //chain.cancel(); call this to abort the promise and releated request
fonte
Porque @jib rejeita minha modificação, então eu posto minha resposta aqui. É apenas o modfify de anwser @ de lança com alguns comentários e usando nomes de variáveis mais compreensíveis.
Abaixo, apenas mostro exemplos de dois métodos diferentes: um é resolve () e o outro é rejeitar ()
let cancelCallback = () => {}; input.oninput = function(ev) { let term = ev.target.value; console.log(`searching for "${term}"`); cancelCallback(); //cancel previous promise by calling cancelCallback() let setCancelCallbackPromise = () => { return new Promise((resolve, reject) => { // set cancelCallback when running this promise cancelCallback = () => { // pass cancel messages by resolve() return resolve('Canceled'); }; }) } Promise.race([setCancelCallbackPromise(), getSearchResults(term)]).then(results => { // check if the calling of resolve() is from cancelCallback() or getSearchResults() if (results == 'Canceled') { console.log("error(by resolve): ", results); } else { console.log(`results for "${term}"`, results); } }); } input2.oninput = function(ev) { let term = ev.target.value; console.log(`searching for "${term}"`); cancelCallback(); //cancel previous promise by calling cancelCallback() let setCancelCallbackPromise = () => { return new Promise((resolve, reject) => { // set cancelCallback when running this promise cancelCallback = () => { // pass cancel messages by reject() return reject('Canceled'); }; }) } Promise.race([setCancelCallbackPromise(), getSearchResults(term)]).then(results => { // check if the calling of resolve() is from cancelCallback() or getSearchResults() if (results !== 'Canceled') { console.log(`results for "${term}"`, results); } }).catch(error => { console.log("error(by reject): ", error); }) } function getSearchResults(term) { return new Promise(resolve => { let timeout = 100 + Math.floor(Math.random() * 1900); setTimeout(() => resolve([term.toLowerCase(), term.toUpperCase()]), timeout); }); }
Search(use resolve): <input id="input"> <br> Search2(use reject and catch error): <input id="input2">
fonte