Różnica między „obietnicą zwrotu” a „obietnicą zwrotu”

108

Biorąc pod uwagę poniższe przykłady kodu, czy jest jakaś różnica w zachowaniu, a jeśli tak, to jakie są te różnice?

return await promise

async function delay1Second() {
  return (await delay(1000));
}

return promise

async function delay1Second() {
  return delay(1000);
}

Jak rozumiem, pierwsza z nich miałaby obsługę błędów w ramach funkcji asynchronicznej, a błędy wypływałyby z obietnicy funkcji asynchronicznej. Jednak druga wymagałaby o jeden tik mniej. Czy to jest poprawne?

Ten fragment jest zwykłą funkcją zwracającą Obietnicę w celach informacyjnych.

function delay(ms) {
  return new Promise((resolve) => {
    setTimeout(resolve, ms);
  });
}
PitaJ
źródło
3
Tak, zredagowałem moje pytanie, ponieważ źle zrozumiałeś moje znaczenie i tak naprawdę nie odpowiadało to, o co się zastanawiałem.
PitaJ
1
@PitaJ: Myślę, że chciałeś usunąć asyncz drugiej ( return promise) próbki.
Stephen Cleary
1
@PitaJ: W takim przypadku twój drugi przykład zwróciłby obietnicę, która jest rozwiązana obietnicą. Raczej dziwny.
Stephen Cleary
5
jakearchibald.com/2017/await-vs-return-vs-return-await to fajny artykuł, który podsumowuje różnice
sanchit
2
@StephenCleary, natknąłem się na to i najpierw pomyślałem dokładnie to samo, obietnica, która jest rozwiązana obietnicą, nie ma tutaj sensu. Ale jak się okazuje, promise.then(() => nestedPromise)spłaszczy się i „podąży” za nestedPromise. Ciekawe, czym różni się od zagnieżdżonych zadań w C #, w których musielibyśmy to Unwrapzrobić. Na marginesie wydaje się, że await somePromise wywołuje Promise.resolve(somePromise).then, a nie tylko somePromise.then, z pewnymi interesującymi różnicami semantycznymi.
noseratio

Odpowiedzi:

155

W większości przypadków nie ma zauważalnej różnicy między returna return await. Obie wersje delay1Secondmają dokładnie to samo obserwowalne zachowanie (ale w zależności od implementacji return awaitwersja może zużywać nieco więcej pamięci, ponieważ Promisemoże zostać utworzony obiekt pośredni ).

Jednak, jak zauważył @PitaJ, jest jeden przypadek, w którym występuje różnica: jeśli returnlub return awaitjest zagnieżdżony w try- catchbloku. Rozważmy ten przykład

async function rejectionWithReturnAwait () {
  try {
    return await Promise.reject(new Error())
  } catch (e) {
    return 'Saved!'
  }
}

async function rejectionWithReturn () {
  try {
    return Promise.reject(new Error())
  } catch (e) {
    return 'Saved!'
  }
}

W pierwszej wersji funkcja async oczekuje odrzuconej obietnicy przed zwróceniem jej wyniku, co powoduje, że odrzucenie zostaje zamienione w wyjątek i catchklauzula zostaje osiągnięta; funkcja zwróci w ten sposób obietnicę składającą się z ciągu „Zapisano!”.

Jednak druga wersja funkcji zwraca odrzuconą obietnicę bezpośrednio, bez oczekiwania na nią w funkcji asynchronicznej , co oznacza, że catchsprawa nie jest wywoływana, a wywołujący otrzymuje odrzucenie.

Denis Washington
źródło
Może też wspomnieć, że ślad stosu byłby inny (nawet bez try / catch)? Myślę, że to jest problem, z którym ludzie spotykają się najczęściej w tym przykładzie:]
Benjamin Gruenbaum
Znalazłem w jednym scenariuszu, że użycie return new Promise(function(resolve, reject) { })w for...ofpętli, a następnie wywołanie resolve()wewnątrz pętli po a pipe()nie wstrzymuje wykonywania programu do zakończenia potoku, zgodnie z życzeniem, jednak użycie await new Promise(...)robi. czy ta ostatnia jest nawet poprawna / poprawna składnia? czy to jest „skrótowe” return await new Promise(...)? czy możesz mi pomóc zrozumieć, dlaczego to drugie działa, a pierwsze nie? w kontekście scenariusza jest solution 02od tej odpowiedzi
user1063287
12

Jak wspomniano w innych odpowiedziach, prawdopodobnie istnieje niewielka poprawa wydajności, gdy obietnica pojawi się, zwracając ją bezpośrednio - po prostu dlatego, że nie musisz najpierw czekać na wynik, a następnie zawijać go ponownie. Jednak nikt jeszcze nie mówił o optymalizacji połączeń końcowych .

Optymalizacja wywołań ogonowych lub „właściwe wywołania ogonowe” to technika, której interpreter używa do optymalizacji stosu wywołań. Obecnie niewiele środowisk wykonawczych jeszcze go obsługuje - mimo że jest technicznie częścią standardu ES6 - ale możliwe jest, że w przyszłości zostanie dodana obsługa, więc możesz się do tego przygotować, pisząc dobry kod w teraźniejszości.

Krótko mówiąc, TCO (lub PTC) optymalizuje stos wywołań, nie otwierając nowej ramki dla funkcji, która jest bezpośrednio zwracana przez inną funkcję. Zamiast tego ponownie wykorzystuje tę samą ramkę.

async function delay1Second() {
  return delay(1000);
}

Ponieważ delay()jest zwracana bezpośrednio przez delay1Second(), środowiska wykonawcze obsługujące PTC najpierw otworzą ramkę dla delay1Second()(funkcji zewnętrznej), ale zamiast otwierać kolejną ramkę dla delay()(funkcji wewnętrznej), po prostu ponownie wykorzystają tę samą ramkę, która została otwarta dla funkcji zewnętrznej. Optymalizuje to stos, ponieważ może zapobiec przepełnieniu stosu (hehe) z bardzo dużymi funkcjami rekurencyjnymi, np fibonacci(5e+25).. Zasadniczo staje się pętlą, która jest znacznie szybsza.

PTC jest aktywowane tylko wtedy, gdy funkcja wewnętrzna jest zwracana bezpośrednio . Nie jest używany, gdy wynik funkcji jest zmieniany przed jego zwróceniem, na przykład jeśli miałeś return (delay(1000) || null)lub return await delay(1000).

Ale jak powiedziałem, większość środowisk wykonawczych i przeglądarek nie obsługuje jeszcze PTC, więc prawdopodobnie nie robi to teraz dużej różnicy, ale nie zaszkodzi, aby Twój kod był zabezpieczony w przyszłości.

Przeczytaj więcej w tym pytaniu: Node.js: Czy istnieją optymalizacje wywołań ogonowych w funkcjach asynchronicznych?

chharvey
źródło
2

To trudne pytanie, ponieważ w praktyce zależy to od tego, jak (prawdopodobnie babel) faktycznie renderuje transpiler async/await. Rzeczy, które są jasne niezależnie:

  • Obie implementacje powinny zachowywać się tak samo, chociaż pierwsza implementacja może mieć o jedną mniej Promisew łańcuchu.

  • Zwłaszcza jeśli awaitusuniesz niepotrzebne , druga wersja nie wymagałaby dodatkowego kodu z transpilera, podczas gdy pierwsza wymaga.

Zatem z punktu widzenia wydajności kodu i debugowania preferowana jest druga wersja, choć tylko nieznacznie, podczas gdy pierwsza wersja ma niewielką korzyść w zakresie czytelności, ponieważ wyraźnie wskazuje, że zwraca obietnicę.

nrabinowitz
źródło
Dlaczego funkcje miałyby zachowywać się tak samo? Pierwsza zwraca rozwiązaną wartość ( undefined), a druga zwraca a Promise.
Amit
4
@Amit obie funkcje zwracają obietnicę
PitaJ
Potw. Dlatego nie mogę znieść async/await- o wiele trudniej mi o tym myśleć. @PitaJ jest poprawne, obie funkcje zwracają obietnicę.
nrabinowitz
A co gdybym otoczył ciało obu funkcji asynchronicznych znakiem try-catch? W takim return promiserazie żaden rejectionnie zostałby złapany, prawda, podczas gdy w tym return await promiseprzypadku byłoby, prawda?
PitaJ
Oba zwracają Obietnicę, ale pierwsza „obiecuje” prymitywną wartość, a druga „obiecuje” Obietnicę. Jeśli wykonasz awaitkażdy z nich w jakiejś witrynie telefonicznej, wynik będzie bardzo różny.
Amit
1

Zauważalna różnica: odrzucenie obietnicy jest obsługiwane w różnych miejscach

  • return somePromiseprzekaże część obietnicy do strony wywołującej, a await inną obietnicę rozliczenia w miejscu wywołania (jeśli taka istnieje). Dlatego jeśli jakaś obietnica zostanie odrzucona, nie będzie obsługiwana przez lokalny blok catch, ale przez blok catch strony wywołania.

async function foo () {
  try {
    return Promise.reject();
  } catch (e) {
    console.log('IN');
  }
}

(async function main () {
  try {
    let a = await foo();
  } catch (e) {
    console.log('OUT');
  }
})();
// 'OUT'

  • return await somePromisenajpierw zaczeka na jakąś obietnicę, że osiądzie lokalnie. Dlatego wartość lub wyjątek będą najpierw obsługiwane lokalnie. => Lokalny blok catch zostanie wykonany, jeśli somePromisezostanie odrzucony.

async function foo () {
  try {
    return await Promise.reject();
  } catch (e) {
    console.log('IN');
  }
}

(async function main () {
  try {
    let a = await foo();
  } catch (e) {
    console.log('OUT');
  }
})();
// 'IN'

Powód: return await Promiseczeka lokalnie i na zewnątrz, return Promiseczeka tylko na zewnątrz

Szczegółowe kroki:

powrót Obietnica

async function delay1Second() {
  return delay(1000);
}
  1. zadzwoń delay1Second();
const result = await delay1Second();
  1. Wewnątrz delay1Second()function delay(1000)zwraca obietnicę natychmiast z [[PromiseStatus]]: 'pending. Nazwijmy to delayPromise.
async function delay1Second() {
  return delayPromise;
// delayPromise.[[PromiseStatus]]: 'pending'
// delayPromise.[[PromiseValue]]: undefined
}
  1. Funkcje asynchroniczne zawijają zwracaną wartość wewnątrz Promise.resolve()( Źródło ). Ponieważ delay1Secondjest to funkcja asynchroniczna, mamy:
const result = await Promise.resolve(delayPromise); 
// delayPromise.[[PromiseStatus]]: 'pending'
// delayPromise.[[PromiseValue]]: undefined
  1. Promise.resolve(delayPromise)zwraca delayPromisebez robienia czegokolwiek, ponieważ dane wejściowe są już obietnicą (zobacz MDN Promise.resolve ):
const result = await delayPromise; 
// delayPromise.[[PromiseStatus]]: 'pending'
// delayPromise.[[PromiseValue]]: undefined
  1. awaitczeka, aż sprawa delayPromisezostanie rozstrzygnięta.
  • JEŚLI delayPromisespełnia PromiseValue = 1:
const result = 1; 
  • ELSE delayPromisejest odrzucane:
// jump to catch block if there is any

powrót czekaj na obietnicę

async function delay1Second() {
  return await delay(1000);
}
  1. zadzwoń delay1Second();
const result = await delay1Second();
  1. Wewnątrz delay1Second()function delay(1000)zwraca obietnicę natychmiast z [[PromiseStatus]]: 'pending. Nazwijmy to delayPromise.
async function delay1Second() {
  return await delayPromise;
// delayPromise.[[PromiseStatus]]: 'pending'
// delayPromise.[[PromiseValue]]: undefined
}
  1. Lokalne oczekiwanie będzie czekać, aż delayPromisezostanie rozliczone.
  • Przypadek 1 : delayPromisejest spełniony przy PromiseValue = 1:
async function delay1Second() {
  return 1;
}
const result = await Promise.resolve(1); // let's call it "newPromise"
const result = await newPromise; 
// newPromise.[[PromiseStatus]]: 'resolved'
// newPromise.[[PromiseValue]]: 1
const result = 1; 
  • Przypadek 2 : delayPromisejest odrzucony:
// jump to catch block inside `delay1Second` if there is any
// let's say a value -1 is returned in the end
const result = await Promise.resolve(-1); // call it newPromise
const result = await newPromise;
// newPromise.[[PromiseStatus]]: 'resolved'
// newPromise.[[PromiseValue]]: -1
const result = -1;

Słownik:

  • Rozlicz: Promise.[[PromiseStatus]]zmiany z pendingna resolvedlubrejected
Ragtime
źródło
0

tutaj zostawiam trochę kodu praktycznego, abyś mógł zrozumieć i to różnicę

 let x = async function () {
  return new Promise((res, rej) => {
    setTimeout(async function () {
      console.log("finished 1");
      return await new Promise((resolve, reject) => { // delete the return and you will see the difference
        setTimeout(function () {
          resolve("woo2");
          console.log("finished 2");
        }, 5000);
      });
      res("woo1");
    }, 3000);
  });
};

(async function () {
  var counter = 0;
  const a = setInterval(function () { // counter for every second, this is just to see the precision and understand the code
    if (counter == 7) {
      clearInterval(a);
    }

    console.log(counter);
    counter = counter + 1;
  }, 1000);
  console.time("time1");
  console.log("hello i starting first of all");
  await x();
  console.log("more code...");
  console.timeEnd("time1");
})();

funkcja "x" jest po prostu funkcją asynchroniczną niż ma inne funkcje, jeśli usunie zwrot, wypisze "więcej kodu ..."

zmienna x jest po prostu funkcją asynchroniczną, która z kolei ma inną funkcję asynchroniczną, w głównym kodzie wywołujemy oczekiwanie na wywołanie funkcji zmiennej x, po zakończeniu następuje zgodnie z sekwencją kodu, to byłoby normalne dla „async / await”, ale wewnątrz funkcji x jest inna funkcja asynchroniczna, która zwraca obietnicę lub zwraca „obietnicę”, pozostanie wewnątrz funkcji x, zapominając o głównym kodzie, to znaczy nie wydrukuje "console.log (" więcej kodu .. "), z drugiej strony, jeśli wstawimy" await ", będzie on czekał na każdą funkcję, która zakończy się i na końcu podąży za normalną sekwencją głównego kodu.

poniżej "console.log (" zakończone 1 ", usuń" powrót ", zobaczysz zachowanie.

Carlos Terrazas
źródło
1
Chociaż ten kod może rozwiązać problem, w tym wyjaśnienie, jak i dlaczego to rozwiązuje problem, naprawdę pomogłoby poprawić jakość twojego posta i prawdopodobnie zaowocowałoby większą liczbą pozytywnych głosów. Pamiętaj, że odpowiadasz na pytanie do czytelników w przyszłości, a nie tylko osoba, która zapyta teraz. Proszę edytować swoje odpowiedzi, aby dodać wyjaśnień i dać wskazówkę co zastosować ograniczenia i założenia.
Brian
0

Oto przykład maszynopisu, który możesz uruchomić i przekonać się, że potrzebujesz tego „return await”

async function  test() {
    try {
        return await throwErr();  // this is correct
        // return  throwErr();  // this will prevent inner catch to ever to be reached
    }
    catch (err) {
        console.log("inner catch is reached")
        return
    }
}

const throwErr = async  () => {
    throw("Fake error")
}


void test().then(() => {
    console.log("done")
}).catch(e => {
    console.log("outer catch is reached")
});

David Dehghan
źródło