Bagaimana Anda menjaga kode dengan kelanjutan / panggilan balik dapat dibaca?

10

Rangkuman: Apakah ada beberapa pola praktik terbaik yang mapan yang dapat saya ikuti untuk menjaga kode saya tetap terbaca meskipun menggunakan kode dan panggilan balik asinkron?


Saya menggunakan perpustakaan JavaScript yang melakukan banyak hal secara tidak sinkron dan sangat bergantung pada panggilan balik. Tampaknya menulis metode "memuat A, memuat B, ..." yang sederhana menjadi cukup rumit dan sulit diikuti dengan menggunakan pola ini.

Biarkan saya memberi contoh (buat). Katakanlah saya ingin memuat banyak gambar (asinkron) dari server web jarak jauh. Di C # / async, saya akan menulis sesuatu seperti ini:

disableStartButton();

foreach (myData in myRepository) {
    var result = await LoadImageAsync("http://my/server/GetImage?" + myData.Id);
    if (result.Success) {
        myData.Image = result.Data;
    } else {
        write("error loading Image " + myData.Id);
        return;
    }
}

write("success");
enableStartButton();

Tata letak kode mengikuti "aliran acara": Pertama, tombol mulai dinonaktifkan, kemudian gambar dimuat ( awaitmemastikan bahwa UI tetap responsif) dan kemudian tombol mulai diaktifkan lagi.

Dalam JavaScript, menggunakan callback, saya menemukan ini:

disableStartButton();

var count = myRepository.length;

function loadImage(i) {
    if (i >= count) {
        write("success");
        enableStartButton();
        return;
    }

    myData = myRepository[i];
    LoadImageAsync("http://my/server/GetImage?" + myData.Id,
        function(success, data) { 
            if (success) {
                myData.Image = data;
            } else {
                write("error loading image " + myData.Id);
                return;
            }
            loadImage(i+1); 
        }
    );
}

loadImage(0);

Saya pikir kekurangannya jelas: saya harus mengolah ulang loop menjadi panggilan rekursif, kode yang seharusnya dieksekusi pada akhirnya adalah suatu tempat di tengah fungsi, kode mulai unduhan ( loadImage(0)) ada di bagian paling bawah, dan umumnya jauh lebih sulit untuk dibaca dan diikuti. Itu jelek dan saya tidak suka itu.

Saya yakin bahwa saya bukan orang pertama yang menghadapi masalah ini, jadi pertanyaan saya adalah: Apakah ada beberapa pola praktik terbaik yang dapat saya ikuti untuk menjaga kode saya tetap terbaca meskipun menggunakan kode asinkron dan panggilan balik?

Heinzi
sumber
Apakah ada alasan tertentu mengapa panggilan "async" Anda harus dilakukan secara berurutan? Apakah ini versi sederhana dari beberapa kode lain?
Izkata
@Izkata: Alasannya adalah saya ingin bersikap baik pada server jarak jauh (= tidak membombardirnya dengan ratusan permintaan simultan). Ini bukan persyaratan yang ditentukan. Ya, ini adalah versi kode yang disederhanakan, LoadImageAsyncsebenarnya adalah panggilan untuk Ext.Ajax.requestSencha Touch.
Heinzi
1
Sebagian besar browser tidak mengizinkan Anda untuk memalu server - mereka hanya mengantri permintaan dan memulai permintaan berikutnya ketika salah satu dari yang sebelumnya selesai.
Izkata
Tuhan! banyak saran buruk di sini. Tidak ada jumlah pola desain yang akan membantu Anda. Lihatlah ke async.js , async.waterfalladalah jawaban Anda.
Salman von Abbas

Jawaban:

4

Sangat tidak mungkin bahwa Anda dapat mencapai dengan tingkat kesederhanaan dan ekspresif yang sama dalam bekerja dengan panggilan balik yang dimiliki C # 5. Compiler melakukan pekerjaan dalam menulis semua boilerplate untuk Anda, dan sampai runtime js akan melakukannya, Anda masih harus melewati callback sesekali di sana-sini.

Namun, Anda mungkin tidak selalu ingin membawa panggilan balik ke tingkat kesederhanaan kode linier - melempar fungsi tidak harus jelek, ada seluruh dunia yang bekerja dengan kode semacam ini, dan mereka tetap waras tanpa asyncdan await.

Misalnya, gunakan fungsi tingkat tinggi (js saya mungkin agak berkarat):

// generic - this is a library function
function iterateAsync(iterator, action, onSuccess, onFailure) {
var item = iterator();
if(item == null) { // exit condition
    onSuccess();
    return;
}
action(item,
    function (success) {
        if(success)
            iterateAsync(iterator, action, onSuccess, onFailure);
        else
            onFailure();
    });
}


// calling code
var currentImage = 0;
var imageCount = 42;

// you know your library function expects an iterator with no params, 
// and an async action with the current item and its continuation as params
iterateAsync(
// this is your iterator
function () {   
    if(currentImage >= imageCount)
        return null;
    return "http://my/server/GetImage?" + (currentImage++);
},

// this is your action - coincidentally, no adaptor for the correct signature is necessary
LoadImageAsync,

// these are your outs
function () { console.log("All OK."); },
function () { console.log("FAILED!"); }
);
vski
sumber
2

Butuh sedikit waktu untuk memecahkan kode mengapa Anda melakukannya dengan cara ini, tapi saya pikir ini mungkin dekat dengan yang Anda inginkan?

function loadImages() {
   var countRemainingToLoad = 0;
   var failures = 0;

   myRepository.each(function (myData) {
      countRemainingToLoad++;

      LoadImageAsync("http://my/server/GetImage?" + myData.Id,
        function(success, data) {
            if (success) {
                myData.Image = data;
            } else {
                write("error loading image " + myData.Id);
                failures++;
            }
            countRemainingToLoad--;
            if (countRemainingToLoad == 0 && failures == 0) {
                enableStartButton();
            }
        }
    );
}

disableStartButton();
loadImages();

Pertama-tama, sebanyak permintaan AJAX yang dapat dilakukan secara bersamaan, dan menunggu sampai semuanya selesai sebelum mengaktifkan tombol Start. Ini akan lebih cepat daripada menunggu berurutan, dan, saya pikir, jauh lebih mudah untuk diikuti.

EDIT : Perhatikan bahwa ini mengasumsikan Anda telah .each()tersedia, dan itu myRepositoryadalah array. Berhati-hatilah dengan iterasi pengulangan yang Anda gunakan di sini, jika tidak tersedia - yang ini memanfaatkan properti penutupan untuk callback. Saya tidak yakin apa yang Anda miliki, karena LoadImageAsynctampaknya menjadi bagian dari perpustakaan khusus - Saya tidak melihat hasil di Google.

Izkata
sumber
+1, saya sudah .each()tersedia, dan, sekarang setelah Anda menyebutkannya, tidak sepenuhnya diperlukan untuk melakukan pemuatan secara berurutan. Saya pasti akan mencoba solusi Anda. (Meskipun saya akan menerima jawaban vski, karena lebih dekat dengan pertanyaan asli, yang lebih umum.)
Heinzi
@Heinzi Setuju pada betapa berbedanya itu, tapi (saya pikir) ini juga merupakan contoh yang layak tentang bagaimana bahasa yang berbeda memiliki cara yang berbeda untuk menangani hal yang sama. Jika sesuatu terasa canggung ketika menerjemahkannya ke bahasa lain, mungkin ada cara yang lebih mudah untuk melakukannya dengan menggunakan paradigma yang berbeda.
Izkata
1

Penafian: jawaban ini tidak menjawab masalah Anda secara khusus, itu adalah jawaban umum untuk pertanyaan: "Apakah ada beberapa pola praktik terbaik yang dapat saya ikuti untuk menjaga kode saya tetap terbaca meskipun menggunakan kode asinkron dan panggilan balik?"

Dari apa yang saya tahu, tidak ada pola "mapan" untuk menangani ini. Namun, saya telah melihat dua jenis metode yang digunakan untuk menghindari mimpi buruk callback bersarang.

1 / Menggunakan fungsi bernama alih-alih panggilan balik anonim

    function start() {
        mongo.findById( id, handleDatas );
    }

    function handleDatas( datas ) {
        // Handle the datas returned.
    }

Dengan cara ini, Anda menghindari sarang dengan mengirimkan logika fungsi anonim di fungsi lain.

2 / Menggunakan perpustakaan manajemen aliran. Saya suka menggunakan Langkah , tapi itu hanya masalah preferensi. Omong-omong, itulah yang digunakan LinkedIn.

    Step( {
        function start() {
            // the "this" magically sends to the next function.
            mongo.findById( this );
        },

        function handleDatas( el ) {
            // Handle the datas.
            // Another way to use it is by returning a value,
            // the value will be sent to the next function.
            // However, this is specific to Step, so look at
            // the documentation of the library you choose.
            return value;
        },

        function nextFunction( value ) {
            // Use the returned value from the preceding function
        }
    } );

Saya menggunakan pustaka manajemen aliran ketika saya menggunakan banyak panggilan balik bersarang, karena jauh lebih mudah dibaca ketika ada banyak kode yang menggunakannya.

Florian Margaine
sumber
0

Sederhananya, JavaScript tidak memiliki gula sintaksis await.
Tetapi memindahkan bagian "ujung" ke bagian bawah fungsi itu mudah; dan dengan fungsi anonim yang dijalankan secara tidak langsung, kita dapat menghindari menyatakan referensi untuk itu.

disableStartButton();

(function(i, count) {
    var loadImage = arguments.callee;
    myData = myRepository[i];

    LoadImageAsync("http://my/server/GetImage?" + myData.Id,
        function(success, data) { 
            if (!success) {
                write("error loading image " + myData.Id);

            } else {
                myData.Image = data;
                if (i < count) {
                    loadImage(i + 1, count);

                } else {
                    write("success");
                    enableStartButton();
                    return;

                }

            }

        }
    );
})(0, myRepository.length);

Anda juga bisa melewati bagian "end" sebagai panggilan balik sukses ke fungsi.

Gipsy King
sumber