Javascript obiecuje ciekawość


96

Kiedy nazywam tę obietnicę, dane wyjściowe nie są zgodne z sekwencją wywołań funkcji. .thenPrzychodzi przed .catch, choć obietnica ze .thenbył nazywany po. Jaki jest tego powód?

const verifier = (a, b) =>
  new Promise((resolve, reject) => (a > b ? resolve(true) : reject(false)));

verifier(3, 4)
  .then((response) => console.log("response: ", response))
  .catch((error) => console.log("error: ", error));

verifier(5, 4)
  .then((response) => console.log("response: ", response))
  .catch((error) => console.log("error: ", error));

wynik

node promises.js
response: true
error: false

34
Nigdy nie powinieneś polegać na czasach między niezależnymi łańcuchami obietnic.
Bergi

Odpowiedzi:


136

To fajne pytanie, żeby dojść do sedna.

Kiedy to robisz:

verifier(3,4).then(...)

która zwraca nową obietnicę, która wymaga kolejnego cyklu z powrotem do pętli zdarzeń, zanim ta nowo odrzucona obietnica będzie mogła uruchomić następującą .catch()procedurę obsługi. Ten dodatkowy cykl daje następną sekwencję:

verifier(5,4).then(...)

szansa na uruchomienie swojego .then()handlera przed poprzednią linią, .catch()ponieważ znajdował się on już w kolejce zanim .catch()handler z pierwszego dostał się do kolejki i pozycje są uruchamiane z kolejki w kolejności FIFO.


Zwróć uwagę, że jeśli użyjesz .then(f1, f2)formularza zamiast znaku .then().catch(), zostanie on uruchomiony, gdy tego oczekujesz, ponieważ nie ma dodatkowej obietnicy, a zatem nie wymaga dodatkowego tiku:

const verifier = (a, b) =>
  new Promise((resolve, reject) => (a > b ? resolve(true) : reject(false)));

verifier(3, 4)
  .then((response) => console.log("response (3,4): ", response),
        (error) => console.log("error (3,4): ", error)
  );

verifier(5, 4)
  .then((response) => console.log("response (5,4): ", response))
  .catch((error) => console.log("error (5,4): ", error));

Uwaga: oznaczyłem również wszystkie wiadomości etykietami, abyś mógł zobaczyć, z jakiego verifier()wywołania pochodzą, co znacznie ułatwia odczytanie wyników.


Specyfikacja ES6 dotycząca zamawiania oddzwaniania i bardziej szczegółowych wyjaśnień

Specyfikacja ES6 mówi nam, że „zadania” obietnicy (jak wywołuje wywołanie zwrotne z a .then()lub .catch()) są uruchamiane w kolejności FIFO w oparciu o to, kiedy są wstawiane do kolejki zadań. Nie nazywa konkretnie FIFO, ale określa, że ​​nowe zadania są wstawiane na końcu kolejki, a zadania są uruchamiane od początku kolejki. To implementuje porządkowanie FIFO.

PerformPromiseThen (który wykonuje wywołanie zwrotne z .then()) doprowadzi do EnqueueJob, który jest sposobem zaplanowania rzeczywistego uruchomienia procedury obsługi rozwiązywania lub odrzucania. EnqueueJob określa, że ​​oczekujące zadanie jest dodawane z tyłu kolejki zadań. Następnie operacja NextJob ściąga element z początku kolejki. Zapewnia to porządek FIFO w obsłudze zadań z kolejki zadań Promise.

Tak więc w przykładzie w pierwotnym pytaniu otrzymujemy wywołania zwrotne dla verifier(3,4)obietnicy i verifier(5,4)obietnicy wstawionej do kolejki zadań w kolejności, w jakiej zostały uruchomione, ponieważ obie pierwotne obietnice są wykonane. Następnie, gdy interpreter wraca do pętli zdarzeń, najpierw podejmuje verifier(3,4)zadanie. Ta obietnica została odrzucona i nie ma do niej oddzwaniania w verifier(3,4).then(...). A zatem odrzuca verifier(3,4).then(...)zwróconą obietnicę i powoduje verifier(3,4).then(...).catch(...)wstawienie procedury obsługi do jobQueue.

Następnie wraca do pętli zdarzeń i następnym zadaniem, które pobiera z jobQueue, jest verifier(5, 4)zadanie. Ma rozwiązaną obietnicę i procedurę obsługi rozstrzygania, więc wywołuje tę procedurę obsługi. Powoduje response (5,4):to pokazanie wyjścia.

Następnie wraca do pętli zdarzeń, a następne zadanie, które pobiera z jobQueue, to verifier(3,4).then(...).catch(...)zadanie, w którym je uruchamia, co powoduje error (3,4)wyświetlenie danych wyjściowych.

Dzieje się tak dlatego, że .catch()w pierwszym łańcuchu jest o jeden poziom obietnicy głębiej w swoim łańcuchu niż .then()w drugim łańcuchu, który powoduje zgłoszenie kolejności. Dzieje się tak dlatego, że łańcuchy obietnic przechodzą z jednego poziomu na drugi przez kolejkę zadań w kolejności FIFO, a nie synchronicznie.


Ogólne zalecenia dotyczące polegania na tym poziomie szczegółowości harmonogramu

FYI, generalnie, staram się pisać kod, który nie zależy od tego poziomu szczegółowej wiedzy o synchronizacji. Chociaż jest to ciekawe i czasami przydatne do zrozumienia, jest to kruchy kod, ponieważ prosta pozornie nieszkodliwa zmiana w kodzie może prowadzić do zmiany względnego czasu. Tak więc, jeśli synchronizacja między dwoma łańcuchami jest krytyczna, wolałbym napisać kod w sposób, który wymusza synchronizację w taki sposób, w jaki chcę, niż polegać na tym poziomie szczegółowego zrozumienia.


Mówiąc dokładniej, to dokładne zachowanie nie jest nigdzie udokumentowane w specyfikacji obietnic, co czyni to szczegółem implementacji. Możesz uzyskać różne zachowanie między tłumaczami (np. Node.js vs Edge vs Firefox) lub między wersjami interpreterów (np. Node 12 vs Node 14). Specyfikacja mówi tylko, że obietnice są przetwarzane asynchronicznie, aby uniknąć kodu zalgo (który IMHO był błędny BTW, ponieważ był motywowany przez ludzi, którzy zadawali takie pytania, chcąc polegać na czasie potencjalnie asynchronicznego kodu)
slebetman

@slebetman - Czy nie jest udokumentowane, że wywołania zwrotne obietnic z oddzielnych obietnic nazywane są FIFO w zależności od tego, kiedy zostały wstawione do kolejki i nie mogą działać do następnego tiku? Wydaje się, że zamawianie FIFO to wszystko, czego tutaj potrzeba, ponieważ .then()musi zwrócić nową obietnicę, która sama musi rozwiązać / odrzucić asynchronicznie w przyszłym tiku, co prowadzi do tego uporządkowania. Czy znasz jakąś implementację, która nie korzysta z porządkowania FIFO konkurencyjnych wywołań zwrotnych?
jfriend00

3
@slebetman Promises / A + nie określa tego. ES6 to określa. (ES11 zmienił jednak zachowanie await).
Bergi

Ze specyfikacji ES6 w kolejności kolejkowania. PerformPromiseThendoprowadzi do EnqueueJobtego, jak zaplanowano wywołanie procedury obsługi rozwiązania lub odrzucenia. EnqueueJob określa, że ​​oczekujące zadanie jest dodawane z tyłu kolejki zadań. Następnie operacja NextJob ściąga element z początku kolejki. Zapewnia to porządek FIFO w kolejce zadań Promise.
jfriend00

@Bergi Jaka jest ta zmiana awaitw ES11? Wystarczy link. Dzięki!!
Pedro A

49

Promise.resolve()
  .then(() => console.log('a1'))
  .then(() => console.log('a2'))
  .then(() => console.log('a3'))
Promise.resolve()
  .then(() => console.log('b1'))
  .then(() => console.log('b2'))
  .then(() => console.log('b3'))

Zamiast wyjścia a1, a2, a3, b1, b2, b3 zobaczysz a1, b1, a2, b2, a3, b3 z tego samego powodu - co potem zwraca obietnicę i przechodzi do końca pętli zdarzeń kolejka. Więc możemy zobaczyć ten „wyścig obietnic”. To samo dotyczy zagnieżdżonych obietnic.

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.