コールバックとPromiseの間に根本的な違いは本当にありますか?


94

シングルスレッドの非同期プログラミングを行うとき、私がよく知っている2つの主なテクニックがあります。最も一般的なのは、コールバックを使用することです。つまり、コールバック関数をパラメーターとして非同期に動作する関数に渡すことを意味します。非同期操作が終了すると、コールバックが呼び出されます。

jQueryこのように設計されたいくつかの典型的なコード:

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

ただし、このタイプのコードは、前のコードが終了したときに追加の非同期呼び出しを次々に行いたい場合、面倒で高度にネストされる可能性があります。

2番目のアプローチはPromiseを使用することです。Promiseは、まだ存在しない可能性のある値を表すオブジェクトです。値にコールバックを設定できます。コールバックは、値を読み取る準備ができたときに呼び出されます。

Promiseと従来のコールバックアプローチの違いは、非同期メソッドがクライアントがコールバックを設定するPromiseオブジェクトを同期的に返すようになったことです。たとえば、AngularJSでPromiseを使用した同様のコード:

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

だから私の質問は次のとおりです。実際に本当の違いはありますか?違いは純粋に構文上のようです。

ある技術を他の技術よりも使用する深い理由はありますか?


8
はい:コールバックは単なる第一級の機能です。Promiseは、値に対する操作をチェーン化する構成可能なメカニズムを提供するモナドであり、便利なインターフェイスを提供するためにコールバックで高階関数を使用することがあります。
アモン


5
@gnat:2つの質問/回答の相対的な質を考えると、重複投票はIMHOの反対の方法でなければなりません。
バートヴァンインゲンシェナウ

回答:


110

約束は単なる構文上の砂糖であると言ってもいいでしょう。コールバックでできる約束でできることはすべて。実際、ほとんどのプロミス実装は、必要に応じて2つの間で変換する方法を提供します。

約束がしばしば良い理由は、それらがより構成可能だということです。これは、複数の約束を組み合わせると「うまくいく」ことを意味しますが、複数のコールバックを組み合わせることはしばしばそうではないことを意味します。たとえば、promiseを変数に割り当てて、後で追加のハンドラーを追加したり、すべてのpromiseが解決した後にのみ実行される大きなグループのpromiseにハンドラーを追加したりするのは簡単です。コールバックを使用してこれらのことをエミュレートすることはできますが、多くのコードが必要になり、正しく実行するのが非常に難しく、最終結果の保守性がはるかに低くなります。

約束が構成可能になる最大の(そして最も微妙な)方法の1つは、戻り値とキャッチされない例外の均一な処理です。コールバックでは、多くのネストされたコールバックのどれが例外をスローしたか、コールバックを取得する関数のどれがその実装にtry / catchを持っているかに完全に依存します。promiseを使用すると、1つのコールバック関数をエスケープする例外がキャッチされ、またはで指定したエラーハンドラーに渡されることがわかります。.error().catch()

単一のコールバックと単一のプロミスを指定した例では、大きな違いはありません。無数の約束に対して無数のコールバックがある場合、約束ベースのコードはより良く見える傾向があります。


ここで、promiseを使用して記述された仮想コードと、次に説明するコールバックを使用して、私が話していることを理解できるようにするための複雑なコードを試みます。

約束あり:

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);
}

コールバックあり:

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);
                }
            });
        });
    });
}

コールバックバージョンでは、約束がなくてもコードの重複を減らす賢い方法があるかもしれませんが、私が考えることができるものはすべて、非常に約束のようなものを実装することに要約できます。


1
promiseのもう1つの大きな利点は、async / awaitまたはyielded promiseの約束された値を返すコルーチンを使用して、さらに「糖化」を受けやすいことです。ここでの利点は、実行する非同期操作の数が異なる可能性があるネイティブ制御フロー構造を混在させることができることです。これを示すバージョンを追加します。
acjay

9
コールバックとプロミスの基本的な違いは、制御の反転です。コールバックでは、APIはコールバックを受け入れる必要がありますが、Promiseでは、APIはpromiseを提供する必要があります。これが主な違いであり、API設計に広範な影響を及ぼします。
cwharris

@ChristopherHarrisは私が同意するかどうかわからない。then(callback)(このコールバックを受け入れるAPIのメソッドの代わりに)コールバックを受け入れるPromiseのメソッドを持つことは、IoCで何もする必要がありません。Promiseは、構成、チェーン、およびエラー処理(有効な鉄道指向プログラミング)に役立つ1レベルの間接参照を導入しますが、コールバックはまだクライアントによって実行されないため、実際にはIoCが存在しません。
dragan.stepanovic

1
@ dragan.stepanovicそのとおりです。間違った用語を使用しました。違いは間接性です。コールバックでは、結果に対して何を行う必要があるかをすでに知っている必要があります。約束があれば、後で決めることができます。
-cwharris
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.