Apakah benar-benar ada perbedaan mendasar antara panggilan balik dan Janji?

94

Saat melakukan pemrograman asinkron single-threaded, ada dua teknik utama yang saya kenal. Yang paling umum adalah menggunakan callback. Itu berarti meneruskan ke fungsi yang bertindak secara tidak sinkron sebagai fungsi panggil balik sebagai parameter. Ketika operasi asinkron akan selesai, panggilan balik akan dipanggil.

Beberapa jQuerykode tipikal dirancang dengan cara ini:

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

Namun jenis kode ini bisa menjadi berantakan dan sangat bersarang ketika kita ingin membuat panggilan async tambahan satu demi satu ketika yang sebelumnya selesai.

Jadi pendekatan kedua menggunakan Janji. Janji adalah sebuah objek yang mewakili nilai yang mungkin belum ada. Anda dapat mengatur callback di atasnya, yang akan dipanggil ketika nilainya siap dibaca.

Perbedaan antara Janji dan pendekatan panggilan balik tradisional, adalah bahwa metode async sekarang secara sinkron mengembalikan objek Janji, di mana klien mengaktifkan panggilan balik. Misalnya, kode serupa menggunakan Janji di AngularJS:

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

Jadi pertanyaan saya adalah: apakah sebenarnya ada perbedaan nyata? Perbedaannya tampaknya murni sintaksis.

Apakah ada alasan yang lebih dalam untuk menggunakan satu teknik di atas yang lain?

Aviv Cohn
sumber
8
Ya: callback hanya fungsi kelas satu. Janji adalah monad yang menyediakan mekanisme yang dapat dikomposisikan untuk rantai operasi pada nilai-nilai, dan kebetulan menggunakan fungsi tingkat tinggi dengan callback untuk menyediakan antarmuka yang nyaman.
amon
5
@gnat: Mengingat kualitas relatif dari dua pertanyaan / jawaban, suara rangkap harus menjadi sebaliknya IMHO.
Bart van Ingen Schenau

Jawaban:

110

Adalah adil untuk mengatakan bahwa janji hanyalah gula sintaksis. Segala sesuatu yang dapat Anda lakukan dengan janji yang dapat Anda lakukan dengan panggilan balik. Bahkan, sebagian besar implementasi janji memberikan cara untuk mengkonversi antara keduanya kapan pun Anda inginkan.

Alasan mendalam mengapa janji sering kali lebih baik adalah bahwa mereka lebih tenang , yang secara kasar berarti bahwa menggabungkan beberapa janji "hanya berfungsi", sementara menggabungkan beberapa panggilan balik sering kali tidak. Sebagai contoh, itu sepele untuk menetapkan janji ke variabel dan melampirkan penangan tambahan untuk nanti, atau bahkan melampirkan penangan ke sekelompok besar janji yang dieksekusi hanya setelah semua janji selesai. Meskipun Anda dapat mengemulasi hal-hal ini dengan callback, dibutuhkan lebih banyak kode, sangat sulit untuk dilakukan dengan benar, dan hasil akhirnya biasanya jauh lebih tidak dapat dipertahankan.

Salah satu cara terbesar (dan paling halus) yang dijanjikan mendapatkan kompabilitas mereka adalah dengan penanganan nilai pengembalian yang sama dan pengecualian yang tidak tertangkap. Dengan panggilan balik, bagaimana pengecualian ditangani mungkin sepenuhnya bergantung pada mana dari banyak panggilan balik bersarang yang melemparkannya, dan fungsi mana yang menerima panggilan balik yang memiliki coba / tangkap dalam implementasinya. Dengan janji, Anda tahu bahwa pengecualian yang lolos dari satu fungsi panggilan balik akan ditangkap dan diteruskan ke penangan kesalahan yang Anda berikan .error()atau .catch().

Misalnya, Anda memberikan satu panggilan balik versus satu janji, memang benar tidak ada perbedaan yang signifikan. Justru ketika Anda memiliki miliaran panggilan balik versus satu miliar janji, kode berbasis janji cenderung terlihat jauh lebih baik.


Berikut adalah upaya pada beberapa kode hipotetis yang ditulis dengan janji dan kemudian dengan panggilan balik yang seharusnya cukup kompleks untuk memberi Anda beberapa gagasan tentang apa yang saya bicarakan.

Dengan Janji:

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

Dengan Callback:

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

Mungkin ada beberapa cara pintar untuk mengurangi duplikasi kode dalam versi callback bahkan tanpa janji, tetapi semua yang saya bisa pikirkan untuk menerapkan sesuatu yang sangat menjanjikan.

Ixrec
sumber
1
Keuntungan utama lain dari janji adalah bahwa mereka dapat menerima "sugarifikasi" lebih lanjut dengan async / waiting atau coroutine yang mengembalikan nilai-nilai yang dijanjikan untuk yieldjanji-janji. Keuntungan di sini adalah bahwa Anda mendapatkan kemampuan untuk mencampur dalam struktur aliran kontrol asli, yang dapat bervariasi dalam berapa banyak operasi async yang mereka lakukan. Saya akan menambahkan versi yang menunjukkan ini.
acjay
9
Perbedaan mendasar antara panggilan balik dan janji adalah inversi kontrol. Dengan panggilan balik, API Anda harus menerima panggilan balik , tetapi dengan Janji, API Anda harus memberikan janji . Ini adalah perbedaan utama, dan memiliki implikasi luas untuk desain API.
cwharris
@ChristopherHarris tidak yakin saya setuju. memiliki then(callback)metode pada Janji yang menerima panggilan balik (bukan metode pada API yang menerima panggilan balik ini) tidak harus melakukan apa pun dengan IoC. Promise memperkenalkan satu tingkat tipuan yang berguna untuk komposisi, rantai, dan penanganan kesalahan (pemrograman berorientasi kereta api pada dasarnya), tetapi panggilan balik masih tidak dilakukan oleh klien, jadi tidak benar-benar tidak adanya IoC.
dragan.stepanovic
1
@ dragan.stepanovic Anda benar, dan saya menggunakan terminologi yang salah. Perbedaannya adalah tipuan. Dengan panggilan balik, Anda harus sudah tahu apa yang harus dilakukan dengan hasilnya. Dengan janji, Anda bisa memutuskan nanti.
cwharris