Używam ES6 Promises do zarządzania wszystkimi moimi danymi w sieci i są sytuacje, w których muszę wymusić ich anulowanie.
Zasadniczo scenariusz jest taki, że mam wyszukiwanie z wyprzedzeniem w interfejsie użytkownika, w którym żądanie jest delegowane do zaplecza, musi przeprowadzić wyszukiwanie na podstawie częściowych danych wejściowych. Chociaż to żądanie sieciowe (nr 1) może zająć trochę czasu, użytkownik kontynuuje wpisywanie, co ostatecznie wyzwala kolejne wywołanie zaplecza (nr 2)
Tutaj numer 2 ma oczywiście pierwszeństwo przed numerem 1, więc chciałbym anulować żądanie opakowania obietnicy nr 1. Mam już pamięć podręczną wszystkich obietnic w warstwie danych, więc teoretycznie mogę ją odzyskać, próbując złożyć obietnicę nr 2.
Ale jak mogę anulować obietnicę nr 1, gdy odzyskam ją z pamięci podręcznej?
Czy ktoś mógłby zasugerować podejście?
źródło
Odpowiedzi:
Nie. Jeszcze nie możemy tego zrobić.
Obietnice ES6 nie obsługują jeszcze anulowania . Jest w drodze, a jego projekt to coś, nad czym naprawdę ciężko pracowało wiele osób. Semantyka eliminacji dźwięku jest trudna do ustalenia, a prace nad tym trwają. Trwają interesujące debaty na temat repozytorium „fetch”, esdiscussów i kilku innych repozytoriów na GH, ale byłbym cierpliwy, gdybym był tobą.
Ale, ale, ale… odwołanie jest naprawdę ważne!
To znaczy, rzeczywistość jest taka, że anulowanie jest naprawdę ważnym scenariuszem w programowaniu po stronie klienta. Przypadki, które opisujesz jako przerywanie żądań sieci Web, są ważne i są wszędzie.
Więc ... język mnie przeleciał!
Tak, przepraszam za to. Obietnice musiały pojawić się jako pierwsze, zanim dalsze rzeczy zostały określone - więc weszły bez pewnych przydatnych rzeczy, takich jak
.finally
i.cancel
- jednak jest w drodze do specyfikacji przez DOM. Anulowanie to nie tylko refleksja, to tylko ograniczenie czasowe i bardziej iteracyjne podejście do projektowania interfejsu API.Więc co mogę zrobić?
Masz kilka alternatyw:
Korzystanie z biblioteki innej firmy jest dość oczywiste. Jeśli chodzi o token, możesz sprawić, aby Twoja metoda przyjęła funkcję, a następnie ją wywołała:
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; }); };
Co pozwoliłoby ci:
var token = {}; var promise = getWithCancel("/someUrl", token); // later we want to abort the promise: token.cancel();
Twój rzeczywisty przypadek użycia -
last
Nie jest to zbyt trudne z podejściem tokenowym:
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); }; }
Co pozwoliłoby ci:
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 });
I nie, biblioteki takie jak Bacon i Rx nie „błyszczą” tutaj, ponieważ są bibliotekami obserwowalnymi, mają po prostu taką samą przewagę, jaką obietnice mają biblioteki na poziomie użytkownika, ponieważ nie są ograniczone do specyfikacji. Myślę, że będziemy czekać i zobaczyć w ES2016, kiedy obserwowalne staną się natywne. Oni są ładne na wpisywanie znaków z wyprzedzeniem chociaż.
źródło
Standardowe propozycje dotyczące anulowanych obietnic nie powiodły się.
Obietnica nie jest powierzchnią kontrolną dla spełniającej ją czynności asynchronicznej; myli właściciela z konsumentem. Zamiast tego utwórz funkcje asynchroniczne, które można anulować za pomocą przekazanego tokenu.
Kolejna obietnica to dobry token, dzięki czemu anulowanie jest łatwe do wdrożenia dzięki
Promise.race
:Przykład: użyj,
Promise.race
aby anulować efekt poprzedniego łańcucha: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">
Tutaj „anulujemy” poprzednie wyszukiwania, wprowadzając
undefined
wynik i testując go, ale możemy łatwo wyobrazić sobie odrzucenie za pomocą"CancelledError"
.Oczywiście nie powoduje to anulowania wyszukiwania w sieci, ale jest to ograniczenie
fetch
. Gdybyfetch
przyjąć obietnicę anulowania jako argument, mogłoby to anulować aktywność sieciową.Ja proponuje to „Cancel” na wzór obietnicy es-dyskutować, dokładnie na to, że
fetch
to zrobić.źródło
Sprawdziłem odniesienie do Mozilla JS i znalazłem to:
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/race
Sprawdźmy to:
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 });
Mamy tutaj p1 i p2 wstawione
Promise.race(...)
jako argumenty, to w rzeczywistości tworzenie nowej obietnicy rozwiązania, czego potrzebujesz.źródło
W przypadku Node.js i Electron zdecydowanie polecam używanie Promise Extensions dla JavaScript (Prex) . Jego autor Ron Buckton jest jednym z kluczowych inżynierów TypeScript, a także autorem aktualnej propozycji anulowania ECMAScript TC39 . Biblioteka jest dobrze udokumentowana i są szanse, że część Prex spełni standard.
Osobiście i wywodząc się z C # tła, bardzo podoba mi się fakt, że Prex jest wzorowany na istniejącym frameworku Cancellation in Managed Threads , tj. W oparciu o podejście przyjęte z
CancellationTokenSource
/CancellationToken
. Z mojego doświadczenia wynika, że były one bardzo przydatne do implementowania niezawodnej logiki anulowania w zarządzanych aplikacjach.Sprawdziłem również, czy działa w przeglądarce, łącząc Prex za pomocą Browserify .
Oto przykład opóźnienia z anulowaniem ( Gist i RunKit , używając Prex do jego
CancellationToken
iDeferred
):// 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}`));
Pamiętaj, że odwołanie to wyścig. Oznacza to, że obietnica mogła zostać pomyślnie rozwiązana, ale zanim ją wypełnisz (za pomocą
await
lubthen
), anulowanie również mogło zostać uruchomione. To od Ciebie zależy, jak poradzisz sobie z tym wyścigiem, ale nie boli wezwanietoken.throwIfCancellationRequested()
dogrywki, tak jak ja powyżej.źródło
Ostatnio miałem podobny problem.
Miałem klienta opartego na obietnicach (nie sieciowego) i zawsze chciałem podawać użytkownikowi najnowsze dane, o które prosił, aby interfejs użytkownika był płynny.
Po zmaga się z anulowania pomysłu,
Promise.race(...)
aPromise.all(..)
ja dopiero się zaczęła moja ostatnia prośba pamiętając identyfikatora i gdy obietnica została spełniona ja renderowania tylko moje dane, gdy dopasowane id ostatnią prośbę.Mam nadzieję, że to komuś pomoże.
źródło
Zobacz https://www.npmjs.com/package/promise-abortable
źródło
Możesz odmówić obietnicy przed zakończeniem:
// 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
Niestety połączenie zostało już wykonane, więc zobaczysz rozwiązanie połączenia na karcie Sieć. Twój kod po prostu go zignoruje.
źródło
Korzystając z podklasy Promise dostarczonej przez pakiet zewnętrzny, można to zrobić w następujący sposób: Demo na żywo
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
źródło
Ponieważ @jib odrzuca moją modyfikację, więc zamieszczam tutaj swoją odpowiedź. To tylko modyfikacja odpowiedzi @ jib z komentarzami i użycie bardziej zrozumiałych nazw zmiennych.
Poniżej pokazuję tylko przykłady dwóch różnych metod: jedna to solution (), druga to odrzucanie ()
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">
źródło