Como você mantém o código com continuações / retornos de chamada legíveis?

10

Resumo: Existem alguns padrões de práticas recomendadas bem estabelecidos que posso seguir para manter meu código legível, apesar de usar código assíncrono e retornos de chamada?


Estou usando uma biblioteca JavaScript que faz várias coisas de forma assíncrona e depende muito de retornos de chamada. Parece que escrever um método simples "carregar A, carregar B, ..." se torna bastante complicado e difícil de seguir usando esse padrão.

Deixe-me dar um exemplo (artificial). Digamos que eu queira carregar um monte de imagens (de forma assíncrona) de um servidor web remoto. Em C # / assíncrono, eu escreveria algo como isto:

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

O layout do código segue o "fluxo de eventos": primeiro, o botão Iniciar é desativado, as imagens são carregadas ( awaitgarante que a interface do usuário permaneça responsiva) e, em seguida, o botão Iniciar é ativado novamente.

Em JavaScript, usando retornos de chamada, eu vim com isso:

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

Eu acho que as desvantagens são óbvias: tive que refazer o loop em uma chamada recursiva, o código que deveria ser executado no final fica em algum lugar no meio da função, o código que inicia o download ( loadImage(0)) está bem no fundo, e geralmente é muito mais difícil de ler e seguir. É feio e eu não gosto.

Tenho certeza de que não sou o primeiro a encontrar esse problema, então minha pergunta é: Existem alguns padrões de práticas recomendadas bem estabelecidos que posso seguir para manter meu código legível, apesar de usar código assíncrono e retornos de chamada?

Heinzi
fonte
Existe uma razão específica para que suas chamadas "assíncronas" tenham que ser feitas seqüencialmente? Esta é uma versão simplificada de outro código?
precisa saber é o seguinte
@ Izkata: O motivo é que eu queria ser legal com o servidor remoto (= não bombardeá-lo com centenas de solicitações simultâneas). Não é um requisito definido em pedra. Sim, é uma versão simplificada do código, LoadImageAsyncé de fato uma chamada Ext.Ajax.requestdo Sencha Touch.
Heinzi
11
A maioria dos navegadores não permite que você martele o servidor de qualquer maneira - eles apenas enfileiram as solicitações e iniciam a próxima quando uma das anteriores é concluída.
precisa saber é o seguinte
Deus! muitos conselhos ruins aqui. Nenhuma quantidade de padrões de design irá ajudá-lo. Olhe para async.js , async.waterfallé a sua resposta.
Salman von Abbas

Respostas:

4

É altamente improvável que você consiga com js comuns o mesmo nível de concisão e expressividade ao trabalhar com retornos de chamada que o C # 5 possui. O compilador faz o trabalho de escrever todo esse clichê para você, e até que o js runtimes faça isso, você ainda precisará passar um retorno de chamada ocasional aqui e ali.

No entanto, nem sempre você pode querer trazer retornos de chamada para o nível de simplicidade do código linear - a execução de funções ao redor não precisa ser feia, há um mundo inteiro trabalhando com esse tipo de código e elas são sãs sem asynce await.

Por exemplo, use funções de ordem superior (meus js podem estar um pouco enferrujados):

// 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
fonte
2

Demorei um pouco para decodificar por que você está fazendo dessa maneira, mas acho que isso pode estar próximo do que você quer?

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

Ele inicia o primeiro número de solicitações AJAX que pode fazer simultaneamente e aguarda até que todas sejam concluídas antes de ativar o botão Iniciar. Isso será mais rápido do que uma espera seqüencial e, eu acho, é muito mais fácil de seguir.

EDIT : Observe que isso pressupõe que você tenha .each()disponível e que myRepositoryé uma matriz. Cuidado com a iteração de loop que você usa aqui, se não estiver disponível - ela tira proveito das propriedades de fechamento do retorno de chamada. Porém, não tenho certeza do que você tem disponível, pois LoadImageAsyncparece fazer parte de uma biblioteca especializada. Não vejo resultados no Google.

Izkata
fonte
+1, eu tenho .each()disponível e, agora que você mencionou, não é estritamente necessário fazer a carga sequencialmente. Definitivamente vou tentar sua solução. (Embora eu vou aceitar a resposta de Vski, já que é mais perto do original, questão mais geral.)
Heinzi
@ Heinzi Concordou em quão diferente é, mas (acho) esse também é um exemplo decente de como diferentes idiomas têm maneiras diferentes de lidar com a mesma coisa. Se algo parecer estranho ao traduzi-lo para um idioma diferente, provavelmente há uma maneira mais fácil de fazer isso usando um paradigma diferente.
precisa saber é o seguinte
1

Isenção de responsabilidade: esta resposta não responde especificamente ao seu problema, é uma resposta genérica à pergunta: "Existem alguns padrões de práticas recomendadas bem estabelecidos que posso seguir para manter meu código legível, apesar de usar código assíncrono e retornos de chamada?"

Pelo que sei, não existe um padrão "bem estabelecido" para lidar com isso. No entanto, vi dois tipos de métodos usados ​​para evitar os pesadelos de retornos de chamada aninhados.

1 / Usando funções nomeadas em vez de retornos de chamada anônimos

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

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

Dessa forma, você evita o aninhamento enviando a lógica da função anônima em outra função.

2 / Usando uma biblioteca de gerenciamento de fluxo. Eu gosto de usar o Step , mas é apenas uma questão de preferência. É o que o LinkedIn usa, a propósito.

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

Uso uma biblioteca de gerenciamento de fluxo quando uso muitos retornos de chamada aninhados, porque é muito mais legível quando há muito código usando-o.

Florian Margaine
fonte
0

Simplificando, o JavaScript não tem o açúcar sintático de await.
Mas mover a parte "final" para a parte inferior da função é fácil; e com uma função anônima em execução imediata, podemos evitar declarar uma referência a ela.

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

Você também pode passar a parte "end" como um retorno de chamada bem-sucedido para a função.

Rei cigano
fonte