Czy naprawdę istnieje zasadnicza różnica między wywołaniami zwrotnymi a obietnicami?

94

Podczas programowania jednowątkowego programowania asynchronicznego znane mi są dwie główne techniki. Najczęstszym z nich jest użycie wywołań zwrotnych. Oznacza to przekazanie do funkcji, która działa asynchronicznie jako funkcja zwrotna jako parametr. Po zakończeniu operacji asynchronicznej wywołanie zwrotne zostanie wywołane.

Niektóre typowe jQuerykody zaprojektowane w ten sposób:

$.get('userDetails', {'name': 'joe'}, function(data) {
    $('#userAge').text(data.age);
});

Jednak ten typ kodu może być nieporządny i mocno zagnieżdżony, gdy chcemy wykonywać kolejne wywołania asynchroniczne jeden po drugim, gdy zakończy się poprzedni.

Drugim podejściem jest obietnica. Obietnica to obiekt reprezentujący wartość, która może jeszcze nie istnieć. Możesz ustawić wywołania zwrotne, które będą wywoływane, gdy wartość będzie gotowa do odczytu.

Różnica między obietnicami a tradycyjnym podejściem do wywołań zwrotnych polega na tym, że metody asynchroniczne teraz synchronicznie zwracają obiekty Promise, na które klient ustawia wywołanie zwrotne. Na przykład podobny kod przy użyciu Promises w AngularJS:

$http.get('userDetails', {'name': 'joe'})
    .then(function(response) {
        $('#userAge').text(response.age);
    });

Więc moje pytanie brzmi: czy rzeczywiście istnieje prawdziwa różnica? Różnica wydaje się być czysto składniowa.

Czy istnieje głębszy powód, aby stosować jedną technikę nad drugą?

Aviv Cohn
źródło
8
Tak: wywołania zwrotne są tylko pierwszorzędnymi funkcjami. Obietnice to monady, które zapewniają mechanizm umożliwiający łączenie operacji na wartościach i używają funkcji wyższego rzędu z wywołaniami zwrotnymi w celu zapewnienia wygodnego interfejsu.
amon
5
@gnat: Biorąc pod uwagę względną jakość dwóch pytań / odpowiedzi, głosowanie w duplikacie powinno być odwrotnie.
Bart van Ingen Schenau

Odpowiedzi:

110

Można śmiało powiedzieć, że obietnice to tylko cukier syntaktyczny. Wszystko, co możesz zrobić z obietnicami, które możesz zrobić dzięki callbackom. W rzeczywistości większość obiecujących implementacji zapewnia sposoby konwertowania między nimi w dowolnym momencie.

Głównym powodem, dla którego obietnice są często lepsze, jest to, że łatwiej je skomponować , co z grubsza oznacza, że ​​łączenie wielu obietnic „po prostu działa”, podczas gdy łączenie wielu połączeń zwrotnych często nie. Na przykład banalne jest przypisanie obietnicy do zmiennej i dołączenie do niej dodatkowych procedur obsługi, a nawet dołączenie procedury obsługi do dużej grupy obietnic, która zostanie wykonana dopiero po spełnieniu wszystkich obietnic. Chociaż można w pewien sposób emulować te rzeczy za pomocą wywołań zwrotnych, wymaga to znacznie więcej kodu, jest bardzo trudne do prawidłowego wykonania, a wynik końcowy jest zwykle o wiele trudniejszy do utrzymania.

Jednym z największych (i najsubtelniejszych) sposobów, w jakie obietnice zyskują swoją zdolność do kompozytu, jest jednolita obsługa wartości zwracanych i nieprzechwycone wyjątki. W przypadku wywołań zwrotnych sposób obsługi wyjątku może zależeć całkowicie od tego, która z wielu zagnieżdżonych wywołań zwrotnych go wyrzuciła, i która funkcja odbierająca wywołanie zwrotne ma próbę / catch w swojej implementacji. Dzięki obietnicom wiesz, że wyjątek, który wymyka się jednej funkcji zwrotnej, zostanie przechwycony i przekazany do procedury obsługi błędów dostarczonej z .error()lub .catch().

W podanym przykładzie pojedynczego wywołania zwrotnego w porównaniu z jedną obietnicą, to prawda, że ​​nie ma znaczącej różnicy. To kiedy masz zillion callback w porównaniu do zillion obietnic, kod oparty na obietnicach wygląda na znacznie ładniejszy.


Oto próba jakiegoś hipotetycznego kodu napisanego z obietnicami, a następnie z wywołaniami zwrotnymi, które powinny być na tyle skomplikowane, aby dać ci pojęcie o czym mówię.

Z obietnicami:

createViewFilePage(fileDescriptor) {
    getCurrentUser().then(function(user) {
        return isUserAuthorizedFor(user.id, VIEW_RESOURCE, fileDescriptor.id);
    }).then(function(isAuthorized) {
        if(!isAuthorized) {
            throw new Error('User not authorized to view this resource.'); // gets handled by the catch() at the end
        }
        return Promise.all([
            loadUserFile(fileDescriptor.id),
            getFileDownloadCount(fileDescriptor.id),
            getCommentsOnFile(fileDescriptor.id),
        ]);
    }).then(function(fileData) {
        var fileContents = fileData[0];
        var fileDownloads = fileData[1];
        var fileComments = fileData[2];
        fileTextAreaWidget.text = fileContents.toString();
        commentsTextAreaWidget.text = fileComments.map(function(c) { return c.toString(); }).join('\n');
        downloadCounter.value = fileDownloads;
        if(fileDownloads > 100 || fileComments.length > 10) {
            hotnessIndicator.visible = true;
        }
    }).catch(showAndLogErrorMessage);
}

Z oddzwanianiem:

createViewFilePage(fileDescriptor) {
    setupWidgets(fileContents, fileDownloads, fileComments) {
        fileTextAreaWidget.text = fileContents.toString();
        commentsTextAreaWidget.text = fileComments.map(function(c) { return c.toString(); }).join('\n');
        downloadCounter.value = fileDownloads;
        if(fileDownloads > 100 || fileComments.length > 10) {
            hotnessIndicator.visible = true;
        }
    }

    getCurrentUser(function(error, user) {
        if(error) { showAndLogErrorMessage(error); return; }
        isUserAuthorizedFor(user.id, VIEW_RESOURCE, fileDescriptor.id, function(error, isAuthorized) {
            if(error) { showAndLogErrorMessage(error); return; }
            if(!isAuthorized) {
                throw new Error('User not authorized to view this resource.'); // gets silently ignored, maybe?
            }

            var fileContents, fileDownloads, fileComments;
            loadUserFile(fileDescriptor.id, function(error, result) {
                if(error) { showAndLogErrorMessage(error); return; }
                fileContents = result;
                if(!!fileContents && !!fileDownloads && !!fileComments) {
                    setupWidgets(fileContents, fileDownloads, fileComments);
                }
            });
            getFileDownloadCount(fileDescriptor.id, function(error, result) {
                if(error) { showAndLogErrorMessage(error); return; }
                fileDownloads = result;
                if(!!fileContents && !!fileDownloads && !!fileComments) {
                    setupWidgets(fileContents, fileDownloads, fileComments);
                }
            });
            getCommentsOnFile(fileDescriptor.id, function(error, result) {
                if(error) { showAndLogErrorMessage(error); return; }
                fileComments = result;
                if(!!fileContents && !!fileDownloads && !!fileComments) {
                    setupWidgets(fileContents, fileDownloads, fileComments);
                }
            });
        });
    });
}

Mogą istnieć sprytne sposoby ograniczenia duplikacji kodu w wersji zwrotnej nawet bez obietnic, ale wszystkie, o których myślę, sprowadzają się do implementacji czegoś bardzo obiecującego.

Ixrec
źródło
1
Inną ważną zaletą obietnic jest to, że są one podatne na dalsze „cukrowanie” za pomocą asynchronizacji / oczekiwania lub korupiny, która przekazuje obiecane wartości dla yieldobietnic ed. Zaletą jest to, że masz możliwość mieszania w natywnych strukturach kontroli sterowania, które mogą różnić się liczbą wykonywanych operacji asynchronicznych. Dodam wersję, która to pokazuje.
acjay
9
Podstawową różnicą między wywołaniami zwrotnymi a obietnicami jest odwrócenie kontroli. W przypadku wywołań zwrotnych interfejs API musi akceptować wywołanie zwrotne , ale w przypadku obietnic interfejs API musi zapewniać obietnicę . Jest to główna różnica i ma szerokie implikacje dla projektowania API.
cwharris
@ChristopherHarris nie jestem pewien, czy się zgodzę. posiadanie then(callback)metody Promise, która akceptuje wywołanie zwrotne (zamiast metody API obsługującej to wywołanie zwrotne), nie musi nic robić z IoC. Promise wprowadza jeden poziom pośredni, który jest użyteczny przy komponowaniu, łańcuchowaniu i obsłudze błędów (w efekcie programowanie zorientowane na kolej), ale klient nie wykonuje jeszcze wywołania zwrotnego, więc tak naprawdę nie ma braku IoC.
dragan.stepanovic
1
@ dragan.stepanovic Masz rację, a ja użyłem złej terminologii. Różnica polega na pośredniości. Dzięki oddzwonieniu musisz już wiedzieć, co należy zrobić z wynikiem. Z obietnicą możesz zdecydować później.
cwharris