Apa itu "callback hell" dan bagaimana dan mengapa RX menyelesaikannya?


113

Dapatkah seseorang memberikan definisi yang jelas bersama dengan contoh sederhana yang menjelaskan apa itu "callback hell" untuk seseorang yang tidak tahu JavaScript dan node.js?

Kapan (dalam jenis pengaturan apa) "masalah callback hell" terjadi?

Mengapa itu terjadi?

Apakah "callback hell" selalu terkait dengan komputasi asinkron?

Atau dapatkah "callback hell" terjadi juga dalam aplikasi berulir tunggal?

Saya mengambil Kursus Reaktif di Coursera dan Erik Meijer berkata dalam salah satu ceramahnya bahwa RX memecahkan masalah "callback hell". Saya bertanya apa itu "callback hell" di forum Coursera tapi saya tidak mendapat jawaban yang jelas.

Setelah menjelaskan "callback hell" pada contoh sederhana, dapatkah Anda juga menunjukkan bagaimana RX memecahkan "masalah callback hell" pada contoh sederhana itu?

Jawaban:


136

1) Apa yang dimaksud dengan "callback hell" untuk seseorang yang tidak tahu javascript dan node.js?

Pertanyaan lain ini memiliki beberapa contoh neraka panggilan balik Javascript: Bagaimana menghindari bersarang lama fungsi asinkron di Node.js

Masalah dalam Javascript adalah bahwa satu-satunya cara untuk "membekukan" komputasi dan "sisanya" mengeksekusi yang terakhir (secara asinkron) adalah dengan meletakkan "sisanya" di dalam callback.

Misalnya, saya ingin menjalankan kode yang terlihat seperti ini:

x = getData();
y = getMoreData(x);
z = getMoreData(y);
...

Apa yang terjadi jika sekarang saya ingin membuat fungsi getData asynchronous, yang berarti saya mendapat kesempatan untuk menjalankan beberapa kode lain sementara saya menunggu mereka mengembalikan nilainya? Dalam Javascript, satu-satunya cara adalah dengan menulis ulang semua yang menyentuh komputasi asinkron menggunakan gaya penerusan lanjutan :

getData(function(x){
    getMoreData(x, function(y){
        getMoreData(y, function(z){ 
            ...
        });
    });
});

Saya rasa saya tidak perlu meyakinkan siapa pun bahwa versi ini lebih jelek dari versi sebelumnya. :-)

2) Kapan (dalam jenis pengaturan apa) "masalah callback hell" terjadi?

Bila Anda memiliki banyak fungsi panggilan balik dalam kode Anda! Semakin sulit untuk bekerja dengan mereka semakin banyak mereka yang Anda miliki di kode Anda dan itu menjadi sangat buruk ketika Anda perlu melakukan loop, blok coba-tangkap dan hal-hal seperti itu.

Misalnya, sejauh yang saya tahu, di JavaScript, satu-satunya cara untuk menjalankan serangkaian fungsi asinkron di mana seseorang dijalankan setelah pengembalian sebelumnya adalah menggunakan fungsi rekursif. Anda tidak dapat menggunakan for loop.

// we would like to write the following
for(var i=0; i<10; i++){
    doSomething(i);
}
blah();

Sebaliknya, kita mungkin harus menulis:

function loop(i, onDone){
    if(i >= 10){
        onDone()
    }else{
        doSomething(i, function(){
            loop(i+1, onDone);
        });
     }
}
loop(0, function(){
    blah();
});

//ugh!

Jumlah pertanyaan yang kami dapatkan di StackOverflow yang menanyakan bagaimana melakukan hal semacam ini adalah bukti betapa membingungkannya :)

3) Mengapa itu terjadi?

Ini terjadi karena di JavaScript, satu-satunya cara untuk menunda komputasi agar berjalan setelah panggilan asinkron kembali adalah dengan meletakkan kode yang tertunda di dalam fungsi callback. Anda tidak dapat menunda kode yang ditulis dalam gaya sinkron tradisional sehingga Anda akan mendapatkan callback bersarang di mana-mana.

4) Atau dapatkah "callback hell" terjadi juga dalam satu aplikasi ulir?

Pemrograman asinkron berkaitan dengan konkurensi sedangkan utas tunggal berkaitan dengan paralelisme. Kedua konsep tersebut sebenarnya bukan hal yang sama.

Anda masih dapat memiliki kode serentak dalam satu konteks berulir. Faktanya, JavaScript, ratu neraka panggilan balik, adalah utas tunggal.

Apa perbedaan antara Concurrency dan Paralelisme?

5) dapatkah Anda juga menunjukkan bagaimana RX memecahkan "masalah callback hell" pada contoh sederhana itu.

Saya tidak tahu apa-apa tentang RX secara khusus, tetapi biasanya masalah ini diselesaikan dengan menambahkan dukungan asli untuk komputasi asinkron dalam bahasa pemrograman. Implementasinya dapat bervariasi dan termasuk: async, generator, coroutines, dan callcc.

Dengan Python kita dapat mengimplementasikan contoh loop sebelumnya dengan sesuatu di sepanjang baris:

def myLoop():
    for i in range(10):
        doSomething(i)
        yield

myGen = myLoop()

Ini bukan kode lengkap tetapi idenya adalah bahwa "hasil" menghentikan perulangan for kami sampai seseorang memanggil myGen.next (). Yang penting adalah kita masih bisa menulis kode menggunakan for loop, tanpa perlu mengeluarkan logika "inside out" seperti yang harus kita lakukan dalam loopfungsi rekursif tersebut .


Jadi callback hell hanya dapat terjadi dalam pengaturan async? Jika kode saya sepenuhnya sinkron (mis. Tidak ada konkurensi) maka "callback hell" tidak dapat terjadi jika saya memahami jawaban Anda dengan benar, apakah itu benar?
jhegedus

Callback hell lebih berkaitan dengan betapa menjengkelkannya kode menggunakan gaya penerusan penerusan. Secara teoritis Anda masih bisa menulis ulang semua fungsi Anda menggunakan gaya CPS bahkan untuk program biasa (artikel wikipedia memiliki beberapa contoh) tetapi, untuk alasan yang bagus, kebanyakan orang tidak melakukannya. Biasanya kita hanya menggunakan gaya penerusan penerusan jika kita terpaksa, yang merupakan kasus untuk pemrograman async Javascript.
hugomg

btw, saya mencari di Google untuk ekstensi reaktif dan saya mendapatkan kesan bahwa mereka lebih mirip dengan pustaka Promise dan bukan ekstensi bahasa yang memperkenalkan sintaksis asinkron. Promises membantu menangani callback bersarang dan dengan penanganan pengecualian tetapi mereka tidak serapi ekstensi sintaks. Perulangan for masih mengganggu kode dan Anda masih perlu menerjemahkan kode dari gaya sinkron ke gaya janji.
hugomg

1
Saya harus menjelaskan bagaimana RX umumnya melakukan pekerjaan yang lebih baik. RX bersifat deklaratif. Anda dapat mendeklarasikan bagaimana program akan menanggapi peristiwa ketika terjadi nanti tanpa mempengaruhi logika program lainnya. Ini memungkinkan Anda untuk memisahkan kode loop utama dari kode penanganan peristiwa. Anda dapat dengan mudah menangani detail seperti pengurutan peristiwa asinkron yang menjadi mimpi buruk saat menggunakan variabel status. Saya menemukan RX adalah implementasi terbersih untuk melakukan permintaan jaringan baru setelah 3 tanggapan jaringan dikembalikan atau kesalahan menangani seluruh rantai jika tidak ada yang kembali. Kemudian dapat mengatur ulang sendiri dan menunggu 3 kejadian yang sama.
pengambilan gambar

Satu lagi komentar terkait: RX pada dasarnya adalah monad lanjutan, yang berhubungan dengan CPS jika saya tidak salah, ini mungkin juga menjelaskan bagaimana / mengapa RX bagus untuk masalah callback / neraka.
jhegedus

30

Jawab saja pertanyaannya: bisakah Anda juga menunjukkan bagaimana RX memecahkan "masalah callback hell" pada contoh sederhana itu?

Keajaiban itu flatMap. Kita dapat menulis kode berikut di Rx untuk contoh @ hugomg:

def getData() = Observable[X]
getData().flatMap(x -> Observable[Y])
         .flatMap(y -> Observable[Z])
         .map(z -> ...)...

Ini seperti Anda menulis beberapa kode FP sinkronis, tetapi sebenarnya Anda dapat membuatnya asinkron dengan Scheduler.


26

Untuk menjawab pertanyaan tentang bagaimana Rx memecahkan neraka panggilan balik :

Pertama mari kita gambarkan callback hell lagi.

Bayangkan sebuah kasus di mana kita harus melakukan http untuk mendapatkan tiga sumber daya - orang, planet, dan galaksi. Tujuan kita adalah menemukan galaksi tempat orang itu tinggal. Pertama kita harus menemukan orangnya, lalu planetnya, lalu galaksi. Itu tiga callback untuk tiga operasi asinkron.

getPerson(person => { 
   getPlanet(person, (planet) => {
       getGalaxy(planet, (galaxy) => {
           console.log(galaxy);
       });
   });
});

Setiap callback bertingkat. Setiap callback batin bergantung pada induknya. Hal ini mengarah pada gaya "piramida malapetaka" dari panggilan balik neraka . Kode tersebut tampak seperti tanda>.

Untuk mengatasi ini di RxJs Anda dapat melakukan sesuatu seperti ini:

getPerson()
  .map(person => getPlanet(person))
  .map(planet => getGalaxy(planet))
  .mergeAll()
  .subscribe(galaxy => console.log(galaxy));

Dengan operator mergeMapAKA, flatMapAnda bisa membuatnya lebih ringkas:

getPerson()
  .mergeMap(person => getPlanet(person))
  .mergeMap(planet => getGalaxy(planet))
  .subscribe(galaxy => console.log(galaxy));

Seperti yang Anda lihat, kode diratakan dan berisi satu rangkaian panggilan metode. Kami tidak memiliki "piramida kehancuran".

Oleh karena itu, neraka panggilan balik dihindari.

Jika Anda bertanya-tanya, promise adalah cara lain untuk menghindari callback hell, tetapi promise sangat menarik , tidak malas seperti yang dapat diamati dan (secara umum) Anda tidak dapat membatalkannya dengan mudah.


Saya bukan pengembang JS, tapi ini penjelasan yang mudah
Omar Beshary

15

Callback hell adalah kode di mana penggunaan fungsi callback dalam kode async menjadi tidak jelas atau sulit diikuti. Umumnya, jika ada lebih dari satu level tipuan, kode yang menggunakan callback bisa menjadi lebih sulit untuk diikuti, lebih sulit untuk difaktor ulang, dan lebih sulit untuk diuji. Bau kode adalah beberapa tingkat lekukan karena melewatkan beberapa lapisan literal fungsi.

Hal ini sering terjadi ketika perilaku memiliki ketergantungan, yaitu ketika A harus terjadi sebelum B harus terjadi sebelum C. Kemudian Anda mendapatkan kode seperti ini:

a({
    parameter : someParameter,
    callback : function() {
        b({
             parameter : someOtherParameter,
             callback : function({
                 c(yetAnotherParameter)
        })
    }
});

Jika Anda memiliki banyak dependensi perilaku dalam kode Anda seperti ini, itu bisa menjadi masalah dengan cepat. Apalagi jika bercabang ...

a({
    parameter : someParameter,
    callback : function(status) {
        if (status == states.SUCCESS) {
          b(function(status) {
              if (status == states.SUCCESS) {
                 c(function(status){
                     if (status == states.SUCCESS) {
                         // Not an exaggeration. I have seen
                         // code that looks like this regularly.
                     }
                 });
              }
          });
        } elseif (status == states.PENDING {
          ...
        }
    }
});

Ini tidak akan berhasil. Bagaimana kita bisa membuat kode asynchronous dieksekusi dalam urutan yang ditentukan tanpa harus melewatkan semua callback ini?

RX adalah kependekan dari 'ekstensi reaktif'. Saya belum pernah menggunakannya, tetapi Googling menyarankan itu adalah kerangka kerja berbasis peristiwa, yang masuk akal. Peristiwa adalah pola umum untuk membuat kode dieksekusi secara berurutan tanpa membuat sambungan yang rapuh . Anda dapat membuat C mendengarkan acara 'bFinished' yang hanya terjadi setelah B dipanggil untuk mendengarkan 'aFinished'. Anda kemudian dapat dengan mudah menambahkan langkah tambahan atau memperluas perilaku semacam ini, dan dapat dengan mudah menguji apakah kode Anda dijalankan secara berurutan hanya dengan menyiarkan peristiwa dalam kasus pengujian Anda.


1

Panggil kembali neraka berarti Anda berada di dalam panggilan balik dari dalam panggilan balik lain dan itu pergi ke panggilan n sampai kebutuhan Anda tidak terpenuhi.

Mari kita pahami melalui contoh panggilan ajax palsu dengan menggunakan set timeout API, mari kita asumsikan kita memiliki API resep, kita perlu mengunduh semua resep.

<body>
    <script>
        function getRecipe(){
            setTimeout(()=>{
                const recipeId = [83938, 73838, 7638];
                console.log(recipeId);
            }, 1500);
        }
        getRecipe();
    </script>
</body>

Pada contoh di atas setelah 1,5 detik ketika timer berakhir di dalam kode panggilan balik akan dijalankan, dengan kata lain, melalui panggilan ajax palsu kita semua resep akan diunduh dari server. Sekarang kita perlu mendownload data resep tertentu.

<body>
    <script>
        function getRecipe(){
            setTimeout(()=>{
                const recipeId = [83938, 73838, 7638];
                console.log(recipeId);
                setTimeout(id=>{
                    const recipe = {title:'Fresh Apple Juice', publisher:'Suru'};
                    console.log(`${id}: ${recipe.title}`);
                }, 1500, recipeId[2])
            }, 1500);
        }
        getRecipe();
    </script>
</body>

Untuk mengunduh data resep tertentu, kami menulis kode di dalam panggilan balik pertama kami dan meneruskan ID resep.

Sekarang katakanlah kita perlu mengunduh semua resep dari penerbit resep yang sama dengan id 7638.

<body>
    <script>
        function getRecipe(){
            setTimeout(()=>{
                const recipeId = [83938, 73838, 7638];
                console.log(recipeId);
                setTimeout(id=>{
                    const recipe = {title:'Fresh Apple Juice', publisher:'Suru'};
                    console.log(`${id}: ${recipe.title}`);
                    setTimeout(publisher=>{
                        const recipe2 = {title:'Fresh Apple Pie', publisher:'Suru'};
                        console.log(recipe2);
                    }, 1500, recipe.publisher);
                }, 1500, recipeId[2])
            }, 1500);
        }
        getRecipe();
    </script>
</body>

Untuk memenuhi kebutuhan kami yaitu mengunduh semua resep nama penerbit suru, kami menulis kode di dalam panggilan balik kedua kami. Jelas kami menulis rantai panggilan balik yang disebut neraka panggilan balik.

Jika Anda ingin menghindari callback hell, Anda bisa menggunakan Promise, yaitu fitur js es6, setiap Promise menerima panggilan balik yang dipanggil saat sebuah promise terisi penuh. janji panggilan balik memiliki dua opsi baik itu diselesaikan atau ditolak. Misalkan panggilan API Anda berhasil, Anda dapat memanggil penyelesaian dan meneruskan data melalui penyelesaian , Anda bisa mendapatkan data ini dengan menggunakan then () . Tetapi jika API Anda gagal, Anda dapat menggunakan reject, gunakan catch untuk menangkap kesalahan. Ingat janji selalu menggunakan kemudian untuk tekad dan menangkap untuk menolak

Mari kita selesaikan masalah callback hell sebelumnya menggunakan sebuah promise.

<body>
    <script>

        const getIds = new Promise((resolve, reject)=>{
            setTimeout(()=>{
                const downloadSuccessfull = true;
                const recipeId = [83938, 73838, 7638];
                if(downloadSuccessfull){
                    resolve(recipeId);
                }else{
                    reject('download failed 404');
                }
            }, 1500);
        });

        getIds.then(IDs=>{
            console.log(IDs);
        }).catch(error=>{
            console.log(error);
        });
    </script>
</body>

Sekarang unduh resep tertentu:

<body>
    <script>
        const getIds = new Promise((resolve, reject)=>{
            setTimeout(()=>{
                const downloadSuccessfull = true;
                const recipeId = [83938, 73838, 7638];
                if(downloadSuccessfull){
                    resolve(recipeId);
                }else{
                    reject('download failed 404');
                }
            }, 1500);
        });

        const getRecipe = recID => {
            return new Promise((resolve, reject)=>{
                setTimeout(id => {
                    const downloadSuccessfull = true;
                    if (downloadSuccessfull){
                        const recipe = {title:'Fresh Apple Juice', publisher:'Suru'};
                        resolve(`${id}: ${recipe.title}`);
                    }else{
                        reject(`${id}: recipe download failed 404`);
                    }

                }, 1500, recID)
            })
        }
        getIds.then(IDs=>{
            console.log(IDs);
            return getRecipe(IDs[2]);
        }).
        then(recipe =>{
            console.log(recipe);
        })
        .catch(error=>{
            console.log(error);
        });
    </script>
</body>

Sekarang kita dapat menulis metode lain memanggil allRecipeOfAPublisher seperti getRecipe yang juga akan mengembalikan sebuah janji, dan kita dapat menulis kemudian () lain untuk menerima janji penyelesaian untuk allRecipeOfAPublisher, saya harap pada titik ini Anda dapat melakukannya sendiri.

Jadi kita belajar bagaimana membuat dan menggunakan promise, sekarang mari kita buat penggunaan promise lebih mudah dengan menggunakan async / await yang diperkenalkan di es8.

<body>
    <script>

        const getIds = new Promise((resolve, reject)=>{
            setTimeout(()=>{
                const downloadSuccessfull = true;
                const recipeId = [83938, 73838, 7638];
                if(downloadSuccessfull){
                    resolve(recipeId);
                }else{
                    reject('download failed 404');
                }
            }, 1500);
        });

        const getRecipe = recID => {
            return new Promise((resolve, reject)=>{
                setTimeout(id => {
                    const downloadSuccessfull = true;
                    if (downloadSuccessfull){
                        const recipe = {title:'Fresh Apple Juice', publisher:'Suru'};
                        resolve(`${id}: ${recipe.title}`);
                    }else{
                        reject(`${id}: recipe download failed 404`);
                    }

                }, 1500, recID)
            })
        }

        async function getRecipesAw(){
            const IDs = await getIds;
            console.log(IDs);
            const recipe = await getRecipe(IDs[2]);
            console.log(recipe);
        }

        getRecipesAw();
    </script>
</body>

Dalam contoh di atas, kami menggunakan fungsi async karena akan berjalan di latar belakang, di dalam fungsi async kami menggunakan kata kunci await sebelum setiap metode yang mengembalikan atau merupakan janji karena menunggu di posisi itu sampai janji itu terpenuhi, dengan kata lain di Kode di bawah sampai getIds selesai diselesaikan atau program tolak akan berhenti mengeksekusi kode di bawah baris itu ketika ID dikembalikan maka kita kembali memanggil fungsi getRecipe () dengan id dan menunggu dengan menggunakan kata kunci menunggu sampai data dikembalikan. Jadi inilah bagaimana akhirnya kami pulih dari neraka panggilan balik.

  async function getRecipesAw(){
            const IDs = await getIds;
            console.log(IDs);
            const recipe = await getRecipe(IDs[2]);
            console.log(recipe);
        }

Untuk menggunakan await kita akan membutuhkan fungsi async, kita bisa mengembalikan sebuah promise jadi gunakan then untuk menyelesaikan promise dan cath untuk menolak promise

dari contoh di atas:

 async function getRecipesAw(){
            const IDs = await getIds;
            const recipe = await getRecipe(IDs[2]);
            return recipe;
        }

        getRecipesAw().then(result=>{
            console.log(result);
        }).catch(error=>{
            console.log(error);
        });

0

Salah satu cara untuk menghindari Callback adalah dengan menggunakan FRP yang merupakan "versi yang disempurnakan" dari RX.

Saya mulai menggunakan FRP baru-baru ini karena saya telah menemukan implementasi yang baik yang disebut Sodium( http://sodium.nz/ ).

Kode tipikal terlihat seperti ini (Scala.js):

def render: Unit => VdomElement = { _ =>
  <.div(
    <.hr,
    <.h2("Note Selector"),
    <.hr,
    <.br,
    noteSelectorTable.comp(),
    NoteCreatorWidget().createNewNoteButton.comp(),
    NoteEditorWidget(selectedNote.updates()).comp(),
    <.hr,
    <.br
  )
}

selectedNote.updates()adalah Streamyang aktif jika selectedNode(yang merupakan Cell) berubah, NodeEditorWidgetkemudian diperbarui secara sesuai.

Jadi, bergantung pada konten selectedNode Cell, yang saat ini diedit Noteakan berubah.

Kode ini menghindari Callback sepenuhnya, hampir, Cacllback-s didorong ke "lapisan luar" / "permukaan" aplikasi, tempat logika penanganan status berinteraksi dengan dunia luar. Tidak diperlukan Callback untuk menyebarkan data dalam logika penanganan status internal (yang mengimplementasikan mesin status).

Kode sumber lengkapnya adalah di sini

Cuplikan kode di atas sesuai dengan contoh Buat / Tampilkan / Perbarui sederhana berikut:

masukkan deskripsi gambar di sini

Kode ini juga mengirimkan pembaruan ke server, sehingga perubahan pada Entitas yang diperbarui disimpan ke server secara otomatis.

Semua penanganan acara ditangani dengan menggunakan Streams danCell s. Ini adalah konsep FRP. Callback hanya diperlukan jika logika FRP berinteraksi dengan dunia luar, seperti input pengguna, mengedit teks, menekan tombol, panggilan AJAX kembali.

Aliran data dijelaskan secara eksplisit, secara deklaratif menggunakan FRP (diimplementasikan oleh pustaka Sodium), jadi tidak ada logika penanganan / panggilan balik peristiwa yang diperlukan untuk mendeskripsikan aliran data.

FRP (yang merupakan versi RX yang lebih "ketat") adalah cara untuk menggambarkan grafik aliran data, yang dapat berisi node yang berisi status. Peristiwa memicu perubahan status dalam status yang berisi node (disebut Cells).

Sodium adalah pustaka FRP orde tinggi, yang berarti bahwa menggunakan flatMap/ switchprimitif dapat mengatur ulang grafik aliran data pada saat runtime.

Saya merekomendasikan untuk melihat ke dalam buku Sodium , ini menjelaskan secara rinci bagaimana FRP menghilangkan semua Callback yang tidak penting untuk menggambarkan logika aliran data yang berkaitan dengan memperbarui status aplikasi sebagai respons terhadap beberapa rangsangan eksternal.

Menggunakan FRP, hanya Callback yang perlu disimpan yang menggambarkan interaksi dengan dunia luar. Dengan kata lain, aliran data dijelaskan secara fungsional / deklaratif ketika seseorang menggunakan kerangka FRP (seperti Sodium), atau ketika seseorang menggunakan kerangka "seperti FRP" (seperti RX).

Sodium juga tersedia untuk Javascript / Typecript.


-3

Jika Anda tidak memiliki pengetahuan tentang callback dan hell callback tidak ada masalah, yang pertama adalah call back dan call back hell. Contoh: hell call back itu seperti kita bisa menyimpan kelas di dalam kelas. tentang itu bersarang dalam bahasa C, C ++. Nested Berarti sebuah kelas di dalam kelas lain.


Jawabannya akan lebih membantu jika berisi potongan kode untuk menunjukkan apa itu 'Callback hell' dan potongan kode yang sama dengan Rx setelah menghapus 'callback hell'
rafa

-4

Gunakan jazz.js https://github.com/Javanile/Jazz.js

itu menyederhanakan seperti ini:

    // jalankan tugas berurutan yang dirantai
    jj.script ([
        // tugas pertama
        fungsi (selanjutnya) {
            // di akhir proses ini 'berikutnya' tunjuk tugas kedua dan jalankan 
            callAsyncProcess1 (selanjutnya);
        },
      // tugas kedua
      fungsi (selanjutnya) {
        // di akhir proses ini 'berikutnya' tunjuk tugas ketiga dan jalankan 
        callAsyncProcess2 (selanjutnya);
      },
      // tugas ketiga
      fungsi (selanjutnya) {
        // di akhir proses ini 'berikutnya' menunjuk ke (jika ada) 
        callAsyncProcess3 (selanjutnya);
      },
    ]);


pertimbangkan ultra-kompak seperti ini github.com/Javanile/Jazz.js/wiki/Script-showcase
cicciodarkast
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.