Utilizo ES6 Promises para administrar toda la recuperación de datos de mi red y hay algunas situaciones en las que necesito forzar su cancelación.
Básicamente, el escenario es tal que tengo una búsqueda de escritura anticipada en la interfaz de usuario donde la solicitud se delega al backend y tiene que realizar la búsqueda en función de la entrada parcial. Si bien esta solicitud de red (n. ° 1) puede llevar un poco de tiempo, el usuario continúa escribiendo, lo que eventualmente activa otra llamada de backend (n. ° 2)
Aquí el n. ° 2 tiene prioridad sobre el n. ° 1, por lo que me gustaría cancelar la solicitud n. ° 1 de cierre de Promise. Ya tengo un caché de todas las Promesas en la capa de datos, por lo que teóricamente puedo recuperarlo mientras intento enviar una Promesa para el n. ° 2.
Pero, ¿cómo cancelo la Promesa # 1 una vez que la recupero del caché?
¿Alguien podría sugerir un enfoque?
fuente
Respuestas:
No. No podemos hacer eso todavía.
ES6 promesas no son compatibles con la cancelación todavía . Está en camino y su diseño es algo en lo que mucha gente trabajó muy duro. Sonido semántica de cancelación de es difícil de hacer bien y esto es un trabajo en progreso. Hay debates interesantes sobre el repositorio "fetch", sobre esdiscuss y sobre varios otros repositorios en GH, pero sería paciente si fuera usted.
Pero, pero, pero ... ¡la cancelación es realmente importante!
Es decir, la realidad del asunto es que la cancelación es realmente un escenario importante en la programación del lado del cliente. Los casos que describe como abortar solicitudes web son importantes y están en todas partes.
Entonces ... ¡el idioma me jodió!
Sí, lo siento por eso. Las promesas tenían que entrar primero antes de que se especificaran más cosas, por lo que entraron sin algunas cosas útiles como
.finally
y.cancel
, sin embargo, está en camino, según las especificaciones a través del DOM. La cancelación no es una ocurrencia tardía, es solo una restricción de tiempo y un enfoque más iterativo para el diseño de API.¿Entonces Que puedo hacer?
Tienes varias alternativas:
Usar una biblioteca de terceros es bastante obvio. En cuanto a un token, puede hacer que su método tome una función y luego la llame, 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; }); };
Lo que te permitiría hacer:
var token = {}; var promise = getWithCancel("/someUrl", token); // later we want to abort the promise: token.cancel();
Su caso de uso real:
last
Esto no es demasiado difícil con el enfoque 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); }; }
Lo que te permitiría hacer:
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 });
Y no, las bibliotecas como Bacon y Rx no "brillan" aquí porque son bibliotecas observables, simplemente tienen la misma ventaja que tienen las bibliotecas de promesa de nivel de usuario al no estar vinculadas a especificaciones. Supongo que esperaremos y veremos en ES2016 cuando los observables se vuelvan nativos. Ellos son ingenioso para typeahead sin embargo.
fuente
Las propuestas estándar de promesas cancelables han fracasado.
Una promesa no es una superficie de control para la acción asincrónica que la cumple; confunde propietario con consumidor. En su lugar, cree funciones asincrónicas que se pueden cancelar mediante algún token pasado.
Otra promesa es un buen token, lo que hace que cancelar sea fácil de implementar con
Promise.race
:Ejemplo: Úselo
Promise.race
para cancelar el efecto de una cadena 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">
Aquí estamos "cancelando" búsquedas anteriores inyectando un
undefined
resultado y probando, pero podríamos imaginar fácilmente rechazar con"CancelledError"
.Por supuesto, esto en realidad no cancela la búsqueda de red, pero esa es una limitación de
fetch
. Sifetch
tomara una promesa de cancelación como argumento, podría cancelar la actividad de la red.He propuesto este "patrón de cancelación de promesa" en es-discus, exactamente para sugerir que
fetch
haga esto.fuente
Revisé la referencia de Mozilla JS y encontré esto:
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/race
Vamos a ver:
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 });
Aquí tenemos p1 y p2
Promise.race(...)
como argumentos, esto en realidad está creando una nueva promesa de resolución, que es lo que necesita.fuente
Para Node.js y Electron, recomiendo encarecidamente utilizar Promise Extensions para JavaScript (Prex) . Su autor, Ron Buckton, es uno de los ingenieros clave de TypeScript y también es el responsable de la propuesta de cancelación ECMAScript actual del TC39 . La biblioteca está bien documentada y es probable que algunos de Prex cumplan con el estándar.
En una nota personal y viniendo de los antecedentes de C #, me gusta mucho el hecho de que Prex se basa en el marco de Cancelación existente en Managed Threads , es decir, basado en el enfoque adoptado con
CancellationTokenSource
/CancellationToken
.NET API. En mi experiencia, han sido muy útiles para implementar una lógica de cancelación sólida en aplicaciones administradas.También verifiqué que funciona dentro de un navegador al empaquetar Prex usando Browserify .
Aquí hay un ejemplo de un retraso con cancelación ( Gist y RunKit , usando Prex para su
CancellationToken
yDeferred
):// 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}`));
Tenga en cuenta que la cancelación es una carrera. Es decir, es posible que una promesa se haya resuelto con éxito, pero en el momento en que la cumpla (con
await
othen
), es posible que la cancelación también se haya activado. Depende de usted cómo maneje esta carrera, pero no está de más pedirtoken.throwIfCancellationRequested()
un tiempo extra, como lo hice anteriormente.fuente
Recientemente me enfrenté a un problema similar.
Tenía un cliente basado en promesas (no uno de red) y quería proporcionar siempre los últimos datos solicitados al usuario para mantener la interfaz de usuario sin problemas.
Después de luchar con la idea cancelación,
Promise.race(...)
yPromise.all(..)
acabo de empezar recordando mi última petición Identificación y cuando se cumplió la promesa que sólo estaba haciendo mis datos cuando coincidían con el ID de una última petición.Espero que ayude a alguien.
fuente
Ver https://www.npmjs.com/package/promise-abortable
fuente
Puedes hacer que la promesa se rechace 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
Lamentablemente, la llamada de recuperación ya se realizó, por lo que verá la llamada resolviéndose en la pestaña Red. Tu código simplemente lo ignorará.
fuente
Usando la subclase Promise proporcionada por el paquete externo, esto se puede hacer de la siguiente manera: Demostración en 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
fuente
Como @jib rechaza mi modificación, publico mi respuesta aquí. Es solo la modificación de la respuesta de @ jib con algunos comentarios y usando nombres de variable más comprensibles.
A continuación, solo muestro ejemplos de dos métodos diferentes: uno es resolver () el otro es rechazar ()
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">
fuente