Как вы читаете код с продолжениями / обратными вызовами?

10

Описание: Существуют ли какие-то хорошо зарекомендовавшие себя шаблоны наилучшей практики, которым я могу следовать, чтобы сохранить мой код читабельным, несмотря на использование асинхронного кода и обратных вызовов?


Я использую библиотеку JavaScript, которая делает много вещей асинхронно и сильно зависит от обратных вызовов. Кажется, что написание простого метода «load A, load B, ...» становится довольно сложным и трудным для понимания при использовании этого шаблона.

Позвольте мне привести (надуманный) пример. Допустим, я хочу загрузить кучу изображений (асинхронно) с удаленного веб-сервера. В 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)), находится в самом низу, и вообще это намного труднее читать и следовать. Это некрасиво, и мне это не нравится.

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

Heinzi
источник
Есть ли конкретная причина, по которой ваши «асинхронные» вызовы должны выполняться последовательно? Это упрощенная версия какого-то другого кода?
Изката
@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!"); }
);
vski
источник
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, сколько он может выполнить одновременно, и ждет, пока все они не будут выполнены, прежде чем активировать кнопку «Пуск». Это будет быстрее, чем последовательное ожидание, и, я думаю, намного легче следовать.

РЕДАКТИРОВАТЬ : Обратите внимание, что это предполагает, что у вас есть в .each()наличии, и myRepositoryэто массив. Будьте внимательны, какую итерацию цикла вы используете здесь вместо этого, если она недоступна - в этом используются свойства замыкания для обратного вызова. Я не уверен, что у вас есть в наличии, так как, LoadImageAsyncкажется, является частью специализированной библиотеки - я не вижу результатов в Google.

Izkata
источник
+1, у меня есть в .each()наличии, и теперь, когда вы упомянули об этом, нет необходимости выполнять загрузку последовательно. Я обязательно попробую ваше решение. (Хотя я приму ответ вски, так как он ближе к оригинальному, более общему вопросу.)
Хайнци
@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);

Вы также можете передать «конечную» часть как функцию обратного вызова для функции.

Цыганский король
источник