Ich verwende ES6 Promises, um alle meine Netzwerkdaten abzurufen, und es gibt Situationen, in denen ich das Abbrechen erzwingen muss.
Grundsätzlich ist das Szenario so, dass ich eine Typ-Ahead-Suche auf der Benutzeroberfläche habe, bei der die Anforderung an das Backend delegiert wird, um die Suche basierend auf der Teileingabe durchzuführen. Während diese Netzwerkanforderung (Nr. 1) einige Zeit in Anspruch nehmen kann, tippt der Benutzer weiter, was schließlich einen weiteren Backend-Aufruf auslöst (Nr. 2).
Hier hat # 2 natürlich Vorrang vor # 1, daher möchte ich die Promise-Wrapping-Anfrage # 1 abbrechen. Ich habe bereits einen Cache aller Versprechen in der Datenschicht, sodass ich ihn theoretisch abrufen kann, wenn ich versuche, ein Versprechen für # 2 einzureichen.
Aber wie kann ich Promise # 1 abbrechen, wenn ich es aus dem Cache abgerufen habe?
Könnte jemand einen Ansatz vorschlagen?
quelle
Antworten:
Nein, das können wir noch nicht.
ES6-Versprechen unterstützen die Stornierung noch nicht . Es ist auf dem Weg und sein Design ist etwas, an dem viele Leute wirklich hart gearbeitet haben. Die Semantik der Tonunterdrückung ist schwer zu korrigieren, und dies ist noch in Arbeit. Es gibt interessante Debatten über das "Fetch" -Repo, über Esdiscuss und über mehrere andere Repos über GH, aber ich wäre nur geduldig, wenn ich Sie wäre.
Aber, aber, aber ... Stornierung ist wirklich wichtig!
Es ist die Realität, dass die Stornierung wirklich ein wichtiges Szenario bei der clientseitigen Programmierung ist. Die Fälle, die Sie als Abbruch von Webanfragen beschreiben, sind wichtig und überall.
Also ... die Sprache hat mich verarscht!
Ja, tut mir leid. Versprechen mussten erst eingehen, bevor weitere Dinge festgelegt wurden - also gingen sie ohne nützliche Dinge wie
.finally
und ein.cancel
- es ist jedoch auf dem Weg zur Spezifikation durch das DOM. Die Stornierung ist kein nachträglicher Gedanke, sondern nur eine zeitliche Beschränkung und ein iterativerer Ansatz für das API-Design.Was kann ich also tun?
Sie haben mehrere Alternativen:
Die Verwendung einer Bibliothek eines Drittanbieters ist ziemlich offensichtlich. Für ein Token können Sie festlegen, dass Ihre Methode eine Funktion übernimmt und sie dann als solche aufruft:
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; }); };
Welches würde Sie tun lassen:
var token = {}; var promise = getWithCancel("/someUrl", token); // later we want to abort the promise: token.cancel();
Ihr tatsächlicher Anwendungsfall -
last
Dies ist mit dem Token-Ansatz nicht allzu schwierig:
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); }; }
Welches würde Sie tun lassen:
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 });
Und nein, Bibliotheken wie Bacon und Rx "glänzen" hier nicht, weil sie beobachtbare Bibliotheken sind. Sie haben nur den gleichen Vorteil, den Bibliotheken auf Benutzerebene versprechen, weil sie nicht spezifikationsgebunden sind. Ich denke, wir werden warten, um in ES2016 zu sehen, wann Observables nativ werden. Sie sind jedoch geschickt für Typeahead.
quelle
Standardvorschläge für stornierbare Versprechen sind gescheitert.
Ein Versprechen ist keine Kontrollfläche für die asynchrone Aktion, die es erfüllt. verwechselt Eigentümer mit Verbraucher. Erstellen Sie stattdessen asynchrone Funktionen , die über ein übergebenes Token abgebrochen werden können.
Ein weiteres Versprechen ist ein gutes Zeichen, das die Implementierung von Abbrechen einfach macht mit
Promise.race
:Beispiel: Verwenden Sie
Promise.race
diese Option, um den Effekt einer vorherigen Kette aufzuheben: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">
Hier "brechen" wir frühere Suchvorgänge ab, indem wir ein
undefined
Ergebnis einfügen und darauf testen, aber wir könnten uns leicht vorstellen,"CancelledError"
stattdessen mit abzulehnen .Natürlich bricht dies die Netzwerksuche nicht wirklich ab, aber das ist eine Einschränkung von
fetch
. Wennfetch
ein Stornierungsversprechen als Argument verwendet wird, kann die Netzwerkaktivität abgebrochen werden.Ich habe vorgeschlagen , diese „Abbrechen Versprechen Muster“ auf es- diskutieren, genau zu deuten darauf hin , dass
fetch
dies tun.quelle
Ich habe die Mozilla JS-Referenz überprüft und Folgendes gefunden:
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/race
Schauen wir uns das an:
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 });
Wir haben hier p1 und p2
Promise.race(...)
als Argumente eingegeben , dies schafft tatsächlich ein neues Lösungsversprechen, was Sie benötigen.quelle
Für Node.js und Electron würde ich die Verwendung von Promise Extensions für JavaScript (Prex) dringend empfehlen . Sein Autor Ron Buckton ist einer der wichtigsten TypeScript-Ingenieure und der Typ, der hinter dem aktuellen ECMAScript-Stornierungsvorschlag des TC39 steht . Die Bibliothek ist gut dokumentiert und es besteht die Möglichkeit, dass einige von Prex dem Standard entsprechen.
Persönlich und aus dem C # -Hintergrund heraus gefällt mir die Tatsache sehr gut, dass Prex dem vorhandenen Framework für die Stornierung in verwalteten Threads nachempfunden ist , dh auf dem Ansatz basiert, der mit
CancellationTokenSource
/CancellationToken
.NET-APIs verfolgt wird. Nach meiner Erfahrung waren diese sehr praktisch, um eine robuste Abbruchlogik in verwalteten Apps zu implementieren.Ich habe auch überprüft, ob es in einem Browser funktioniert, indem ich Prex mit Browserify gebündelt habe .
Hier ist ein Beispiel für eine Verzögerung mit Stornierung ( Gist und RunKit , wobei Prex für sein
CancellationToken
und verwendet wirdDeferred
):// 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}`));
Beachten Sie, dass die Stornierung ein Rennen ist. Das heißt, ein Versprechen wurde möglicherweise erfolgreich gelöst, aber wenn Sie es (mit
await
oderthen
) beobachten, wurde möglicherweise auch die Stornierung ausgelöst. Es liegt an Ihnen, wie Sie mit diesem Rennen umgehen, aber es tut nicht weh,token.throwIfCancellationRequested()
eine Verlängerung anzurufen , wie ich es oben getan habe.quelle
Ich hatte kürzlich ein ähnliches Problem.
Ich hatte einen versprechungsbasierten Client (keinen Netzwerk-Client) und wollte dem Benutzer immer die zuletzt angeforderten Daten geben, um die Benutzeroberfläche reibungslos zu halten.
Nachdem ich mit der Stornierungsidee zu kämpfen hatte
Promise.race(...)
undPromise.all(..)
mich gerade an meine letzte Anforderungs-ID erinnerte und als das Versprechen erfüllt wurde, renderte ich meine Daten nur, wenn sie mit der ID einer letzten Anfrage übereinstimmten.Hoffe es hilft jemandem.
quelle
Siehe https://www.npmjs.com/package/promise-abortable
quelle
Sie können das Versprechen ablehnen, bevor Sie fertig sind:
// 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
Leider wurde der Abruf bereits ausgeführt, sodass der Anruf auf der Registerkarte Netzwerk aufgelöst wird. Ihr Code wird es einfach ignorieren.
quelle
Mit der vom externen Paket bereitgestellten Promise-Unterklasse kann dies wie folgt erfolgen: Live-Demo
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
quelle
Da @jib meine Änderung ablehnt, poste ich meine Antwort hier. Es ist nur die Änderung von @ jibs Antwort mit einigen Kommentaren und der Verwendung verständlicherer Variablennamen.
Im Folgenden zeige ich nur Beispiele für zwei verschiedene Methoden: Eine ist auflösen (), die andere ist ablehnen ()
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">
quelle