Czy obietnice nie są tylko oddzwanianiem?

430

JavaScript rozwijam od kilku lat i wcale nie rozumiem zamieszania związanego z obietnicami.

Wygląda na to, że wszystko, co robię, to zmiana:

api(function(result){
    api2(function(result2){
        api3(function(result3){
             // do work
        });
    });
});

I tak mógłbym użyć biblioteki takiej jak asynchroniczna , z czymś takim jak:

api().then(function(result){
     api2().then(function(result2){
          api3().then(function(result3){
               // do work
          });
     });
});

Co jest bardziej kodem i mniej czytelnym. Nic tu nie zyskałem, nie jest też nagle magicznie „płaskie”. Nie wspominając już o konieczności przekształcania rzeczy w obietnice.

Więc o co tyle zamieszania w związku z obietnicami?

Benjamin Gruenbaum
źródło
11
Na temat : na stronie Html5Rocks
ComFreek
2
Fyi, odpowiedź, którą zaakceptowałeś, to ta sama stara lista trywialnych korzyści, które wcale nie są obietnicami i nawet nie przekonały mnie do korzystania z obietnic:. Tym, co przekonało mnie do korzystania z obietnic, był aspekt DSL opisany w odpowiedzi Oscara
Esailija,
@Esailija dobrze, twój głos mówi przekonał mnie. Przyjąłem inną odpowiedź, chociaż myślę, że Bergi podnosi także kilka naprawdę dobrych (i różnych) punktów.
Benjamin Gruenbaum
@Esailija „To, co przekonało mnie do korzystania z obietnic, to aspekt DSL opisany w odpowiedzi Oscara„ << Co to jest „DSL”? i do jakiego „aspektu DSL” masz na myśli?
monsto
1
@monsto: DSL: język specyficzny dla domeny, język specjalnie zaprojektowany do użycia w określonym podzbiorze systemu (np. SQL lub ORM do komunikowania się z bazą danych, wyrażenia regularne w celu znalezienia wzorców itp.). W tym kontekście „DSL” to API Obietnicy, które, jeśli ustrukturyzujesz swój kod tak, jak to zrobił Oscar, jest prawie jak cukier syntaktyczny, który uzupełnia JavaScript, aby uwzględnić konkretny kontekst operacji asynchronicznych. Obietnice tworzą niektóre idiomy, które zamieniają je w język prawie zaprojektowany, aby umożliwić programiście łatwiejsze uchwycenie nieco nieuchwytnego przepływu mentalnego tego typu struktur.
Michael Ekoka,

Odpowiedzi:

631

Obietnice nie są oddzwanianiem. Obietnica reprezentuje przyszły wynik operacji asynchronicznej . Oczywiście, pisząc je tak, jak robisz, nie zyskujesz zbyt wiele. Ale jeśli napiszesz je zgodnie z przeznaczeniem, możesz napisać kod asynchroniczny w sposób podobny do kodu synchronicznego i łatwiejszy do naśladowania:

api().then(function(result){
    return api2();
}).then(function(result2){
    return api3();
}).then(function(result3){
     // do work
});

Z pewnością nie mniej kodu, ale o wiele bardziej czytelny.

Ale to nie koniec. Odkryjmy prawdziwe korzyści: co zrobić, jeśli chcesz sprawdzić, czy w którymkolwiek z kroków nie ma błędu? Byłoby piekło robić to z oddzwanianiem, ale z obietnicami to bułka z masłem:

api().then(function(result){
    return api2();
}).then(function(result2){
    return api3();
}).then(function(result3){
     // do work
}).catch(function(error) {
     //handle any error that may occur before this point
});

Prawie tak samo jak try { ... } catchblok.

Nawet lepiej:

api().then(function(result){
    return api2();
}).then(function(result2){
    return api3();
}).then(function(result3){
     // do work
}).catch(function(error) {
     //handle any error that may occur before this point
}).then(function() {
     //do something whether there was an error or not
     //like hiding an spinner if you were performing an AJAX request.
});

A nawet lepiej: Co jeśli te rozmowy do 3 api, api2, api3może uruchomić jednocześnie (na przykład gdyby były rozmowy AJAX), ale trzeba było czekać na trzech? Bez obietnic powinieneś stworzyć jakiś licznik. Z obietnicami, używając notacji ES6, jest kolejna bułka z masłem:

Promise.all([api(), api2(), api3()]).then(function(result) {
    //do work. result is an array contains the values of the three fulfilled promises.
}).catch(function(error) {
    //handle the error. At least one of the promises rejected.
});

Mam nadzieję, że teraz widzisz obietnice w nowym świetle.

Oscar Paz
źródło
124
Naprawdę nie powinni nazwać go „Obietnicą”. „Przyszłość” jest co najmniej 100 razy lepsza.
Pacerier
12
@Pacerier, bo jQuery nie skazywał przyszłości?
Esailija,
5
Alternatywny wzorzec (w zależności od tego, co jest pożądane: api (). Następnie (api2). Następnie (api3) .then (doWork); to znaczy, jeśli funkcje api2 / api3 pobierają dane z ostatniego kroku i same zwracają nowe obietnice, one można po prostu przykuty łańcuchem bez dodatkowego owijania. To znaczy, komponują
Dtipson,
1
Co się stanie, jeśli w api2i działają operacje asynchroniczne api3? czy ostatnia .thenzostanie wywołana dopiero po zakończeniu operacji asynchronicznych?
NiCk Newman
8
Dlaczego mnie oznaczyłeś? Właśnie poprawiłem gramatykę. Nie jestem ekspertem od JS. :)
Scott Arciszewski
169

Tak, obietnice są asynchronicznymi wywołaniami zwrotnymi. Nie mogą zrobić niczego, czego nie mogą zrobić wywołania zwrotne, a ty masz do czynienia z tymi samymi problemami z asynchronicznością, co z zwykłymi wywołaniami zwrotnymi.

Jednak obietnice są czymś więcej niż tylko wywołaniami zwrotnymi. Są bardzo potężną abstrakcją, pozwalają na czystszy i lepszy, funkcjonalny kod z mniej podatną na błędy płytą kotłową.

Więc jaki jest główny pomysł?

Obietnice są obiektami reprezentującymi wynik pojedynczego (asynchronicznego) obliczenia. Oni rozwiązać do tego wyniku tylko raz. Jest kilka rzeczy, co to oznacza:

Obietnice wdrażają wzorzec obserwatora:

  • Nie musisz znać wywołań zwrotnych, które wykorzystają tę wartość przed zakończeniem zadania.
  • Zamiast oczekiwać wywołań zwrotnych jako argumentów dla twoich funkcji, możesz łatwo returnstworzyć obiekt Promise
  • Obietnica zapisze wartość i możesz w sposób przezroczysty dodać oddzwonienie, kiedy tylko chcesz. Zostanie wywołany, gdy wynik będzie dostępny. „Przejrzystość” oznacza, że ​​kiedy masz obietnicę i dodajesz do niej wywołanie zwrotne, nie ma znaczenia dla twojego kodu, czy wynik już dotarł - interfejs API i umowy są takie same, znacznie upraszczając buforowanie / zapamiętywanie.
  • Możesz łatwo dodawać wiele połączeń zwrotnych

Obietnice są łańcuchowe ( monadyczne , jeśli chcesz ):

  • Jeśli trzeba przekształcić wartość obietnica oznacza, ty map funkcję przekształcić na obietnicy i wrócić nową obietnicę, że reprezentuje przekształcony wynik. Nie możesz synchronicznie uzyskać wartości, aby jakoś z niej skorzystać, ale możesz łatwo podnieść transformację w kontekście obietnicy. Brak wywołań zwrotnych płyty kotłowej.
  • Jeśli chcesz połączyć dwa zadania asynchroniczne, możesz użyć tej .then()metody. Wywołanie zwrotne zostanie wywołane z pierwszym wynikiem i zwróci obietnicę dotyczącą wyniku obietnicy, którą zwraca wywołanie zwrotne.

Brzmi skomplikowanie? Czas na przykładowy kod.

var p1 = api1(); // returning a promise
var p3 = p1.then(function(api1Result) {
    var p2 = api2(); // returning a promise
    return p2; // The result of p2 …
}); // … becomes the result of p3

// So it does not make a difference whether you write
api1().then(function(api1Result) {
    return api2().then(console.log)
})
// or the flattened version
api1().then(function(api1Result) {
    return api2();
}).then(console.log)

Spłaszczanie nie przychodzi magicznie, ale możesz to łatwo zrobić. W przypadku mocno zagnieżdżonego przykładu (prawie) odpowiednikiem byłoby

api1().then(api2).then(api3).then(/* do-work-callback */);

Jeśli zapoznanie się z kodem tych metod pomaga zrozumieć, oto najbardziej podstawowa biblioteka obietnic w kilku wierszach .

O co tyle zamieszania w obietnicach?

Abstrakcja Promise umożliwia znacznie lepszą kompozycyjność funkcji. Na przykład, obok thenłańcucha, allfunkcja tworzy obietnicę dla połączonego wyniku wielu obietnic oczekiwania równoległego.

Wreszcie, obietnice pochodzą ze zintegrowaną obsługą błędów. Wynik obliczeń może być taki, że albo obietnica zostanie spełniona z wartością, albo zostanie odrzucona z uzasadnionego powodu. Wszystkie funkcje kompozycji obsługują to automatycznie i propagują błędy w łańcuchach obietnic, dzięki czemu nie musisz się tym wszystkim przejmować - w przeciwieństwie do implementacji zwykłego wywołania zwrotnego. Na koniec możesz dodać dedykowane wywołanie zwrotne błędu dla wszystkich występujących wyjątków.

Nie wspominając już o konieczności przekształcania rzeczy w obietnice.

To całkiem trywialne w przypadku dobrych bibliotek obietnic, zobacz Jak przekonwertować istniejące API zwrotne na obietnice?

Bergi
źródło
cześć Bergi, czy chciałbyś coś ciekawego dodać do tego SO pytania? stackoverflow.com/questions/22724883/…
Sebastien Lorber
1
@Sebastien: Nie wiem zbyt wiele o Scali (jeszcze) i mogłem tylko powtórzyć to, co powiedział Benjamin :-)
Bergi
3
Tylko mała uwaga: nie możesz używać .then(console.log), ponieważ console.log zależy od kontekstu konsoli. W ten sposób spowoduje to nielegalny błąd wywołania. Użyj console.log.bind(console)lub, x => console.log(x)aby powiązać kontekst.
Tamas Hegedus,
3
@hege_hegedus: Istnieją środowiska, w których consolemetody są już powiązane. I oczywiście powiedziałem tylko, że oba gniazda mają dokładnie takie samo zachowanie, nie że żadne z nich zadziała :-P
Bergi
1
To było świetne. Właśnie tego potrzebowałem: mniej kodu i więcej interpretacji. Dziękuję Ci.
Adam Patterson
21

Oprócz już ustalonych odpowiedzi, dzięki funkcjom strzałek ES6 Obietnice zmieniają się ze skromnie świecącego małego niebieskiego karła prosto w czerwonego olbrzyma. To ma zamiar zapaść się w supernową:

api().then(result => api2()).then(result2 => api3()).then(result3 => console.log(result3))

Jak zauważył oligofren , bez argumentów między wywołaniami API nie potrzebujesz w ogóle anonimowych funkcji otoki:

api().then(api2).then(api3).then(r3 => console.log(r3))

I wreszcie, jeśli chcesz osiągnąć supermasywny poziom czarnej dziury, możesz oczekiwać obietnic:

async function callApis() {
    let api1Result = await api();
    let api2Result = await api2(api1Result);
    let api3Result = await api3(api2Result);

    return api3Result;
}
John Weisz
źródło
9
„z funkcjami strzałek ES6 Obietnice zmieniają się ze skromnie świecącej małej niebieskiej gwiazdy prosto w czerwonego olbrzyma. To zaraz zapadnie się w supernową” Tłumaczenie: Połączenie funkcji strzałek ES6 z obietnicami jest niesamowite :)
user3344977
3
To sprawia, że ​​Obietnice brzmią jak kosmiczna katastrofa, co nie wydaje mi się twoim intencją.
Michael McGinnis
Jeśli nie używasz argumentów w apiXmetodach, równie dobrze można pominąć funkcje strzałek w sumie: api().then(api2).then(api3).then(r3 => console.log(r3)).
oligofren,
@MichaelMcGinnis - Korzystny wpływ obietnic na nudne piekło zwrotne przypomina wybuchającą supernową w ciemnym kącie kosmosu.
John Weisz
Wiem, że masz na myśli poetycko, ale obietnice są dalekie od „supernowej”. Przychodzi na myśl łamanie prawa monadycznego lub brak wsparcia dla bardziej opłacalnych przypadków użycia, takich jak anulowanie lub zwracanie wielu wartości.
Dmitri Zaitsev,
15

Oprócz świetnych odpowiedzi powyżej można dodać 2 dodatkowe punkty:

1. Różnica semantyczna:

Obietnice mogą zostać rozwiązane już po stworzeniu. Oznacza to, że gwarantują warunki, a nie zdarzenia . Jeśli są już rozwiązane, przekazana do nich funkcja rozstrzygania jest nadal wywoływana.

I odwrotnie, wywołania zwrotne obsługują zdarzenia. Tak więc, jeśli zdarzenie, które Cię interesuje, miało miejsce przed zarejestrowaniem oddzwaniania, oddzwanianie nie jest wywoływane.

2. Odwrócenie kontroli

Oddzwanianie obejmuje odwrócenie kontroli. Po zarejestrowaniu funkcji wywołania zwrotnego w dowolnym interfejsie API środowisko wykonawcze JavaScript zapisuje funkcję wywołania zwrotnego i wywołuje ją z pętli zdarzeń, gdy jest gotowa do uruchomienia.

Patrz Pętla JavaScript zdarzeń o wyjaśnienia.

W przypadku obietnic kontrola znajduje się w programie wywołującym. Metodę .then () można wywołać w dowolnym momencie, jeśli przechowujemy obiekt promise.

dww
źródło
1
Nie wiem dlaczego, ale wydaje się to lepsza odpowiedź.
radiantshaw
13

Oprócz innych odpowiedzi, składnia ES2015 bezproblemowo łączy się z obietnicami, redukując jeszcze więcej kodu podstawowego:

// Sequentially:
api1()
  .then(r1 => api2(r1))
  .then(r2 => api3(r2))
  .then(r3 => {
      // Done
  });

// Parallel:
Promise.all([
    api1(),
    api2(),
    api3()
]).then(([r1, r2, r3]) => {
    // Done
});
Duncan Luk
źródło
5

Obietnice nie są wywołaniami zwrotnymi, oba są programistycznymi idiomami, które ułatwiają programowanie asynchroniczne. Użycie programowania w stylu async / Oczekiwanie przy użyciu coroutines lub generatorów, które zwracają obietnice, można uznać za trzeci taki idiom. Porównanie tych idiomów w różnych językach programowania (w tym Javascript) znajduje się tutaj: https://github.com/KjellSchubert/promise-future-task

Kjell Schubert
źródło
5

Nie, wcale nie.

Wywołania zwrotne to po prostu funkcje w JavaScript, które należy wywoływać, a następnie wykonywać po zakończeniu wykonywania innej funkcji. Jak to się dzieje?

W rzeczywistości w JavaScript funkcje są uważane za obiekty, a zatem jak wszystkie inne obiekty, nawet funkcje mogą być wysyłane jako argumenty do innych funkcji . Najczęstszym i ogólnym przypadkiem użycia, o jakim można pomyśleć, jest funkcja setTimeout () w JavaScript.

Obietnice to nic więcej, ale znacznie bardziej improwizowane podejście do obsługi i strukturyzacji kodu asynchronicznego w porównaniu do robienia tego samego z wywołaniami zwrotnymi.

Obietnica odbiera dwa wywołania zwrotne w funkcji konstruktora: rozwiązywanie i odrzucanie. Te obietnice zwrotne w obietnicach zapewniają nam szczegółową kontrolę nad obsługą błędów i przypadkami powodzenia. Wywołanie zwrotne rozstrzygania jest używane, gdy wykonanie obietnicy zakończyło się powodzeniem, a wywołanie odrzucenia jest używane do obsługi przypadków błędów.

Ayush Jain
źródło
2

Żadne obietnice nie są tylko opakowaniem zwrotnym

przykład Możesz użyć rodzimych obietnic javascript z węzłem js

my cloud 9 code link : https://ide.c9.io/adx2803/native-promises-in-node

/**
* Created by dixit-lab on 20/6/16.
*/

var express = require('express');
var request = require('request');   //Simplified HTTP request client.


var app = express();

function promisify(url) {
    return new Promise(function (resolve, reject) {
    request.get(url, function (error, response, body) {
    if (!error && response.statusCode == 200) {
        resolve(body);
    }
    else {
        reject(error);
    }
    })
    });
}

//get all the albums of a user who have posted post 100
app.get('/listAlbums', function (req, res) {
//get the post with post id 100
promisify('http://jsonplaceholder.typicode.com/posts/100').then(function (result) {
var obj = JSON.parse(result);
return promisify('http://jsonplaceholder.typicode.com/users/' + obj.userId + '/albums')
})
.catch(function (e) {
    console.log(e);
})
.then(function (result) {
    res.end(result);
}
)

})


var server = app.listen(8081, function () {

var host = server.address().address
var port = server.address().port

console.log("Example app listening at http://%s:%s", host, port)

})


//run webservice on browser : http://localhost:8081/listAlbums
Apoorv
źródło
1

Obietnice JavaScript faktycznie używają funkcji zwrotnych, aby określić, co zrobić po rozwiązaniu lub odrzuceniu obietnicy, dlatego obie nie różnią się zasadniczo. Główną ideą obietnic jest przyjmowanie wywołań zwrotnych - zwłaszcza zagnieżdżonych wywołań zwrotnych, w których chcesz wykonać jakieś czynności, ale byłoby to bardziej czytelne.

Hamid Shoja
źródło
0

Przegląd obietnic:

W JS możemy zawijać operacje asynchroniczne (np. Wywołania bazy danych, wywołania AJAX) w obietnice. Zwykle chcemy uruchomić dodatkową logikę na pobranych danych. Obietnice JS mają funkcje obsługi, które przetwarzają wynik operacji asynchronicznych. Funkcje modułu obsługi mogą zawierać nawet inne operacje asynchroniczne, które mogą polegać na wartości poprzednich operacji asynchronicznych.

Obietnica zawsze ma 3 następujące stany:

  1. w toku: stan początkowy każdej obietnicy, niespełniony ani odrzucony.
  2. spełnione: Operacja zakończona powodzeniem.
  3. odrzucone: Operacja nie powiodła się.

Oczekująca obietnica może zostać rozstrzygnięta / wypełniona lub odrzucona za pomocą wartości. Następnie wywoływane są następujące metody obsługi, które przyjmują wywołania zwrotne jako argumenty:

  1. Promise.prototype.then() : Po rozwiązaniu obietnicy zostanie wywołany argument wywołania zwrotnego tej funkcji.
  2. Promise.prototype.catch() : Gdy obietnica zostanie odrzucona, zostanie wywołany argument wywołania zwrotnego tej funkcji.

Mimo że powyższe metody zdobywają argumenty wywołania zwrotnego, są znacznie lepsze niż używanie tylko wywołań zwrotnych tutaj, jest to przykład, który wiele wyjaśni:

Przykład

function createProm(resolveVal, rejectVal) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            if (Math.random() > 0.5) {
                console.log("Resolved");
                resolve(resolveVal);
            } else {
                console.log("Rejected");
                reject(rejectVal);
            }
        }, 1000);
    });
}

createProm(1, 2)
    .then((resVal) => {
        console.log(resVal);
        return resVal + 1;
    })
    .then((resVal) => {
        console.log(resVal);
        return resVal + 2;
    })
    .catch((rejectVal) => {
        console.log(rejectVal);
        return rejectVal + 1;
    })
    .then((resVal) => {
        console.log(resVal);
    })
    .finally(() => {
        console.log("Promise done");
    });

  • Funkcja createProm tworzy obietnice, które są rozpatrywane lub odrzucane na podstawie losowej liczby po 1 sekundzie
  • Jeśli obietnica zostanie rozwiązana, thenwywoływana jest pierwsza metoda, a rozstrzygnięta wartość jest przekazywana jako argument wywołania zwrotnego
  • Jeśli obietnica zostanie odrzucona, catchwywoływana jest pierwsza metoda, a odrzucona wartość jest przekazywana jako argument
  • catchI thenmetody powrotu obietnic, dlatego możemy je łańcuch. Zawijają wszystkie zwracane wartości Promise.resolvei dowolne wartości wyrzucane (przy użyciu throwsłowa kluczowego) w Promise.reject. Tak więc każda zwracana wartość jest przekształcana w obietnicę i na tej obietnicy możemy ponownie wywołać funkcję modułu obsługi.
  • Łańcuchy obietnic dają nam lepszą kontrolę i lepszy przegląd niż zagnieżdżone oddzwaniania. Na przykład catchmetoda obsługuje wszystkie błędy, które wystąpiły przed catchmodułem obsługi.
Willem van der Veen
źródło