Versprechen - Ist es möglich, ein Versprechen zu stornieren?

90

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?

Mondwanderer
quelle
2
Ist das eine Option, ein Äquivalent einer Entprellungsfunktion zu verwenden, um nicht zu oft auszulösen und zu veralteten Anforderungen zu werden? Angenommen, eine Verzögerung von 300 ms würde den Trick machen. Zum Beispiel hat Lodash eine der Implementierungen - lodash.com/docs#debounce
shershen
Dies ist, wenn Dinge wie Bacon und Rx nützlich sind.
Elclanrs
@shershen ja - wir haben dies, aber hier geht es nicht so sehr um das Problem mit der Benutzeroberfläche. Die Serverabfrage kann einige Zeit dauern, daher möchte ich in der Lage sein, die Versprechen abzubrechen ...
Moonwalker
Versuchen Sie Observables von Rxjs
FieryCod

Antworten:

163

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 .finallyund 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:

  • Verwenden Sie eine Drittanbieter-Bibliothek wie Bluebird , die sich viel schneller als die Spezifikation bewegen kann und daher storniert werden kann, sowie eine Reihe anderer Extras - genau das tun große Unternehmen wie WhatsApp.
  • Übergeben Sie eine Löschung Token .

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.

Benjamin Gruenbaum
quelle
24
Benjamin, ich habe es wirklich genossen, deine Antwort zu lesen. Sehr gut durchdacht, strukturiert, artikuliert und mit guten praktischen Beispielen und Alternativen. Wirklich hilfreich. Vielen Dank.
Moonwalker
@FranciscoPresencia-Stornierungs-Token sind als Vorschlag für Stufe 1 unterwegs.
Benjamin Gruenbaum
Wo können wir uns über diese tokenbasierte Stornierung informieren? Wo ist der Vorschlag?
Schaden
@harm der Vorschlag ist in Stufe 1 tot.
Benjamin Gruenbaum
1
Ich liebe Rons Arbeit, aber ich denke, wir sollten ein wenig warten, bevor wir Empfehlungen für Bibliotheken abgeben, die die Leute noch nicht benutzen:] Danke für den Link, obwohl ich ihn mir ansehen werde!
Benjamin Gruenbaum
24

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.racediese 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 undefinedErgebnis 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. Wenn fetchein 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 fetchdies tun.

Ausleger
quelle
@jib warum meine Änderung ablehnen? Ich kläre es nur.
Allenyllee
8

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.

nikola-miljkovic
quelle
Schön - das ist vielleicht genau das, was ich brauche. Ich werde es versuchen.
Moonwalker
Wenn Sie Probleme damit haben, können Sie hier Code einfügen, damit ich Ihnen helfen kann :)
nikola-miljkovic
5
Ich habe es versucht. Nicht ganz da. Dies löst das schnellste Versprechen ... Ich muss immer das zuletzt eingereichte lösen, dh alle älteren Versprechen bedingungslos stornieren.
Moonwalker
1
Auf diese Weise werden alle anderen Versprechen nicht mehr behandelt. Sie können ein Versprechen nicht mehr stornieren.
Nikola-Miljkovic
Ich habe es versucht, das zweite Versprechen (eines in diesem Ex) lässt den Prozess nicht
beenden
3

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 CancellationTokenund verwendet wird Deferred):

// 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 awaitoder then) 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.

noseratio
quelle
1

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(...)und Promise.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.

Igor Słomski
quelle
Slomski die Frage ist nicht, was auf der Benutzeroberfläche angezeigt werden soll. Es geht darum, Versprechen
abzusagen
0

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.

Rashomon
quelle
0

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
Dmitriy Mozgovoy
quelle
-1

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">

Allenyllee
quelle