web-dev-qa-db-ja.com

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

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

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

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

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

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

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

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

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

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

95
Aviv Cohn

約束は単なる構文上の砂糖であると言っても差し支えありません。コールバックで実行できるpromiseで実行できるすべてのこと。実際、ほとんどのpromise実装では、いつでも2つの間で変換する方法が提供されています。

プロミスがよくなる深い理由は、プロミスがより多くcomposeableであることです。つまり、複数のプロミスを組み合わせると「うまくいく」が、複数のコールバックを組み合わせるとうまくいかないことがよくあります。たとえば、変数にプロミスを割り当てて、後で追加のハンドラーをアタッチすることや、すべてのプロミスが解決した後でのみ実行されるプロミスの大きなグループにハンドラーをアタッチすることも簡単です。コールバックを使用してこれらをエミュレートすることはできますが、より多くのコードが必要で、 very を正しく実行するのが難しく、通常、最終結果の保守性ははるかに低くなります。

Promiseが構成可能性を獲得する最大の(そして微妙な)方法の1つは、戻り値とキャッチされない例外を均一に処理することです。コールバックでは、例外がどのように処理されるかは、ネストされた多くのコールバックのどれが例外をスローしたか、コールバックを取得する関数のどれがその実装にtry/catchを持っているかに完全に依存します。約束があれば、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);
                }
            });
        });
    });
}

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

110
Ixrec