J'utilise ES6 Promises pour gérer toutes mes récupérations de données réseau et il y a des situations où je dois forcer leur annulation.
Fondamentalement, le scénario est tel que j'ai une recherche de saisie anticipée sur l'interface utilisateur où la demande est déléguée au backend doit effectuer la recherche basée sur l'entrée partielle. Bien que cette requête réseau (n ° 1) puisse prendre un peu de temps, l'utilisateur continue de taper ce qui déclenche éventuellement un autre appel backend (n ° 2)
Ici, le n ° 2 a naturellement la priorité sur le n ° 1, je voudrais donc annuler la demande d'encapsulation Promise n ° 1. J'ai déjà un cache de toutes les promesses dans la couche de données afin que je puisse théoriquement le récupérer lorsque j'essaie de soumettre une promesse pour # 2.
Mais comment annuler la promesse n ° 1 une fois que je la récupère du cache?
Quelqu'un pourrait-il suggérer une approche?
la source
Réponses:
Non, nous ne pouvons pas encore faire ça.
Promesses ES6 ne prennent pas en charge l' annulation encore . Il est en route et sa conception est quelque chose sur lequel beaucoup de gens ont travaillé très dur. La sémantique d'annulation du son est difficile à maîtriser et c'est un travail en cours. Il y a des débats intéressants sur le repo «fetch», sur esdiscuss et sur plusieurs autres dépôts sur GH mais je serais juste patient si j'étais vous.
Mais, mais, mais ... l'annulation est vraiment importante!
C'est vrai, la réalité de la question est que l'annulation est vraiment un scénario important dans la programmation côté client. Les cas que vous décrivez comme l'annulation de requêtes Web sont importants et ils sont partout.
Alors ... la langue m'a foutu!
Ouais, désolé pour ça. Les promesses devaient être reçues en premier avant que d'autres choses ne soient spécifiées - elles sont donc entrées sans certaines choses utiles comme
.finally
et.cancel
- elles sont en route cependant, vers les spécifications via le DOM. L'annulation n'est pas une réflexion après coup, c'est juste une contrainte de temps et une approche plus itérative de la conception d'API.Alors qu'est-ce que je peux faire?
Vous avez plusieurs alternatives:
L'utilisation d'une bibliothèque tierce est assez évidente. En ce qui concerne un jeton, vous pouvez faire en sorte que votre méthode prenne une fonction, puis l'appelle comme telle:
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; }); };
Ce qui vous permettrait de faire:
var token = {}; var promise = getWithCancel("/someUrl", token); // later we want to abort the promise: token.cancel();
Votre cas d'utilisation réel -
last
Ce n'est pas trop difficile avec l'approche des jetons:
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); }; }
Ce qui vous permettrait de faire:
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 });
Et non, les bibliothèques comme Bacon et Rx ne «brillent» pas ici parce que ce sont des bibliothèques observables, elles ont juste le même avantage que les bibliothèques de promesse de niveau utilisateur ont en n'étant pas liées aux spécifications. Je suppose que nous attendrons d'avoir et de voir dans ES2016 quand les observables deviendront natifs. Ils sont cependant très pratiques pour la tête de frappe.
la source
Les propositions standard de promesses annulables ont échoué.
Une promesse n'est pas une surface de contrôle pour l'action asynchrone qui l'accomplit; confond le propriétaire avec le consommateur. Au lieu de cela, créez des fonctions asynchrones qui peuvent être annulées via un jeton transmis.
Une autre promesse fait un bon jeton, rendant l'annulation facile à implémenter avec
Promise.race
:Exemple: Utilisez
Promise.race
pour annuler l'effet d'une chaîne précédente: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">
Ici, nous "annulons" les recherches précédentes en injectant un
undefined
résultat et en le testant, mais nous pourrions facilement imaginer rejeter avec à la"CancelledError"
place.Bien sûr, cela n'annule pas réellement la recherche sur le réseau, mais c'est une limitation de
fetch
. Sifetch
vous preniez une promesse d'annulation comme argument, cela pourrait annuler l'activité du réseau.J'ai proposé ce "modèle d'annulation de promesse" sur es-discuter, exactement pour suggérer que
fetch
faire ceci.la source
J'ai vérifié la référence Mozilla JS et j'ai trouvé ceci:
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/race
Regardons ça:
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 });
Nous avons ici p1 et p2 mis en
Promise.race(...)
arguments, cela crée en fait une nouvelle promesse de résolution, ce dont vous avez besoin.la source
Pour Node.js et Electron, je recommande vivement d'utiliser les extensions de promesse pour JavaScript (Prex) . Son auteur, Ron Buckton, est l'un des principaux ingénieurs de TypeScript et est également le type derrière la proposition d' annulation ECMAScript actuelle du TC39 . La bibliothèque est bien documentée et il y a des chances que certains de Prex se mettent au standard.
Sur une note personnelle et venant de l'arrière-plan C #, j'aime beaucoup le fait que Prex est modelé sur le framework Cancellation in Managed Threads , c'est-à-dire basé sur l'approche adoptée avec les API
CancellationTokenSource
/CancellationToken
.NET. D'après mon expérience, ceux-ci ont été très pratiques pour implémenter une logique d'annulation robuste dans les applications gérées.Je l'ai également vérifié pour fonctionner dans un navigateur en regroupant Prex à l'aide de Browserify .
Voici un exemple de délai avec annulation ( Gist et RunKit , utilisant Prex pour son
CancellationToken
etDeferred
):// 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}`));
Notez que l'annulation est une course. Par exemple, une promesse peut avoir été résolue avec succès, mais au moment où vous la respectez (avec
await
outhen
), l'annulation peut également avoir été déclenchée. C'est à vous de décider comment vous gérez cette course, mais cela ne fait pas de mal d'appelertoken.throwIfCancellationRequested()
un temps supplémentaire, comme je le fais ci-dessus.la source
J'ai rencontré un problème similaire récemment.
J'avais un client basé sur la promesse (pas un client réseau) et je voulais toujours donner les dernières données demandées à l'utilisateur pour que l'interface utilisateur soit fluide.
Après avoir lutté avec l'idée d'annulation,
Promise.race(...)
etPromise.all(..)
j'ai commencé à me souvenir de mon dernier identifiant de demande et lorsque la promesse a été remplie, je ne rendais mes données que lorsqu'elles correspondaient à l'identifiant d'une dernière demande.J'espère que ça aide quelqu'un.
la source
Voir https://www.npmjs.com/package/promise-abortable
la source
Vous pouvez faire rejeter la promesse avant de terminer:
// 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
Malheureusement, l'appel de récupération a déjà été effectué, vous verrez donc la résolution de l'appel dans l'onglet Réseau. Votre code l'ignorera simplement.
la source
En utilisant la sous-classe Promise fournie par le package externe, cela peut être fait comme suit: Démo en direct
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
la source
Parce que @jib rejette ma modification, je publie donc ma réponse ici. C'est juste la modification de la réponse de @ jib avec quelques commentaires et en utilisant des noms de variables plus compréhensibles.
Ci-dessous, je montre juste des exemples de deux méthodes différentes: l'une est résoudre () l'autre est rejeter ()
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">
la source