Як ви зберігаєте код з читанням продовжень / зворотних викликів?


11

Короткий зміст: Чи існують чітко встановлені моделі найкращої практики, які я можу дотримуватися, щоб не читати код, незважаючи на використання асинхронного коду та зворотних викликів?


Я використовую бібліотеку JavaScript, яка робить багато матеріалів асинхронно і сильно покладається на зворотні дзвінки. Здається, що написання простого методу "навантаження А, завантаження В, ..." стає досить складним і важко дотримуватися, використовуючи цю схему.

Дозвольте навести (надуманий) приклад. Скажімо, я хочу завантажити купу зображень (асинхронно) з віддаленого веб-сервера. У C # / async я б написав щось подібне:

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

Розташування коду слідує за "потоком подій": Спочатку кнопку запуску вимкнено, потім завантажують зображення ( awaitгарантує, що користувальницький інтерфейс залишається чуйним), а потім кнопку запуску знову ввімкнено.

У JavaScript, використовуючи зворотні дзвінки, я придумав таке:

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

Я думаю, що недоліки очевидні: мені довелося переробити цикл у рекурсивний виклик; код, який повинен бути виконаний в кінці кінця, знаходиться десь посередині функції, код, що починає завантаження ( loadImage(0)), знаходиться в самому дні, і взагалі набагато важче читати та слідкувати. Це некрасиво, і мені це не подобається.

Я впевнений, що я не вперше зіткнувся з цією проблемою, тому моє запитання таке: чи існують чітко встановлені моделі найкращої практики, які я можу дотримуватися, щоб не читати код, незважаючи на використання асинхронного коду та зворотних викликів?


Чи є певна причина, коли ваші "асинхронні" дзвінки потрібно робити послідовно? Це спрощена версія якогось іншого коду?
Ізката

@Izkata: Причина в тому, що я хотів бути приємним до віддаленого сервера (= не бомбардувати його сотнями одночасних запитів). Це не вимога, встановлена ​​в камені. Так, це спрощена версія коду, LoadImageAsyncнасправді заклик до Ext.Ajax.requestSencha Touch.
Хайнзі

1
Більшість браузерів не дозволять вам забити сервер у будь-якому випадку - вони просто ставлять у чергу запити та починають наступний, коли завершується один із попередніх.
Ізката


Боже! тут багато поганих порад. Жодна кількість дизайнерських моделей вам не допоможе. Подивіться на async.js , async.waterfallце ваша відповідь.
Салман фон Аббас

Відповіді:


4

Вкрай малоймовірно, що ви можете досягти за допомогою простого js такого ж рівня лаконічності та виразності у роботі з зворотними зворотами, як у C # 5. Компілятор виконує роботу, написавши все те, що вказано для вас, і поки js час виконання цього не зробиться, вам все одно доведеться проходити випадкові зворотні дзвінки тут і там.

Однак, ви не завжди можете звести зворотні дзвінки до рівня простоти лінійного коду - перекидання функцій навколо не повинно бути негарним, весь світ працює з таким кодом, і вони залишаються здоровими без asyncі await.

Наприклад, використовуйте функції вищого порядку (мій js може бути трохи іржавим):

// 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!"); }
);

2

Змусив мене трохи розшифрувати, чому ти робиш це так, але я думаю, це може бути близьким до того, що ти хочеш?

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

Він спочатку запускає стільки запитів AJAX, скільки може робити одночасно, і чекає, поки всі вони будуть виконані, перш ніж увімкнути кнопку "Пуск". Це буде швидше, ніж послідовне очікування, і, я думаю, набагато простіше прослідкувати.

EDIT : Зауважте, що це передбачає, що у вас є .each()доступ, і myRepositoryце масив. Будьте уважні, яку ітерацію циклу ви використовуєте замість цього, якщо вона недоступна - цей використовує властивості закриття для зворотного виклику. Я не впевнений, що у вас є в наявності, оскільки, LoadImageAsyncздається, є частиною спеціалізованої бібліотеки - я не бачу результатів в Google.


+1, у мене є .each()доступні, і тепер, коли ви це згадуєте, не обов'язково робити навантаження послідовно. Я обов’язково спробую ваше рішення. (Хоча я прийму відповідь vski, оскільки це ближче до оригінального, більш загального питання.)
Хайнці

@Heinzi Домовився про те, наскільки він різний, але (я думаю) це також гідний приклад того, як різні мови мають різні способи поводження з одним і тим же. Якщо щось перебуває незручно під час перекладу на іншу мову, мабуть, простіший спосіб зробити це за допомогою іншої парадигми.
Ізката

1

Відмова від відповідальності: ця відповідь не відповідає конкретно вашій проблемі, це загальна відповідь на питання: "Чи існують якісь усталені схеми найкращої практики, які я можу дотримуватися, щоб не читати код, незважаючи на використання асинхронного коду та зворотних викликів?"

З того, що мені відомо, не існує «налагодженої» схеми, яка б впоралася з цим. Однак я бачив два види методів, які використовуються для уникнення вкладених кошмарів зворотного виклику.

1 / Використання названих функцій замість анонімних зворотних дзвінків

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

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

Таким чином, ви уникаєте вкладення, пересилаючи логіку анонімної функції в іншу функцію.

2 / Використання бібліотеки управління потоками. Мені подобається використовувати Step , але це лише питання переваги. Це, до речі, використовується 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
        }
    } );

Я використовую бібліотеку управління потоками, коли використовую багато вкладених зворотних викликів, тому що це набагато читабельніше, коли використовується багато коду.


0

Простіше кажучи, у JavaScript немає синтаксичного цукру await.
Але перемістити "кінцеву" частину в нижню частину функції легко; і миттєво виконуючи анонімну функцію, ми можемо уникати оголошення посилання на неї.

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

Ви також можете передати "кінцеву" частину як успіх-зворотний виклик функції.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.