Skip to content

Controle de fluxo assíncrono

INFO

O material neste post é fortemente inspirado pelo Livro Node.js do Mixu.

Em sua essência, JavaScript é projetado para ser não bloqueante na thread "principal", que é onde as visualizações são renderizadas. Você pode imaginar a importância disso no navegador. Quando a thread principal é bloqueada, resulta no infame "congelamento" que os usuários finais temem, e nenhum outro evento pode ser despachado, resultando na perda de aquisição de dados, por exemplo.

Isso cria algumas restrições únicas que apenas um estilo funcional de programação pode curar. É aqui que os callbacks entram em cena.

No entanto, os callbacks podem se tornar desafiadores de lidar em procedimentos mais complicados. Isso geralmente resulta em "inferno de callbacks", onde múltiplas funções aninhadas com callbacks tornam o código mais desafiador de ler, depurar, organizar, etc.

js
async1(function (input, result1) {
  async2(function (result2) {
    async3(function (result3) {
      async4(function (result4) {
        async5(function (output) {
          // faça algo com output
        });
      });
    });
  });
});

Claro, na vida real, provavelmente haveria linhas adicionais de código para lidar com result1, result2, etc., portanto, o comprimento e a complexidade desse problema geralmente resultam em código que parece muito mais desorganizado do que o exemplo acima.

É aqui que as funções são muito úteis. Operações mais complexas são compostas por muitas funções:

  1. estilo iniciador/entrada
  2. middleware
  3. terminador

O "estilo iniciador/entrada" é a primeira função na sequência. Esta função aceitará a entrada original, se houver, para a operação. A operação é uma série executável de funções, e a entrada original será principalmente:

  1. variáveis em um ambiente global
  2. invocação direta com ou sem argumentos
  3. valores obtidos por solicitações de sistema de arquivos ou rede

Solicitações de rede podem ser solicitações recebidas iniciadas por uma rede externa, por outro aplicativo na mesma rede ou pelo próprio aplicativo na mesma rede ou em uma rede externa.

Uma função middleware retornará outra função, e uma função terminadora invocará o callback. O seguinte ilustra o fluxo para solicitações de rede ou sistema de arquivos. Aqui, a latência é 0 porque todos esses valores estão disponíveis na memória.

js
function final(someInput, callback) {
  callback(`${someInput} e terminado executando o callback `);
}
function middleware(someInput, callback) {
  return final(`${someInput} tocado pelo middleware `, callback);
}
function initiate() {
  const someInput = 'olá, esta é uma função ';
  middleware(someInput, function (result) {
    console.log(result);
    // requer callback para `return` result
  });
}
initiate();

Gerenciamento de estado

Funções podem ou não depender do estado. A dependência de estado surge quando a entrada ou outra variável de uma função depende de uma função externa.

Dessa forma, existem duas estratégias principais para o gerenciamento de estado:

  1. passar variáveis diretamente para uma função, e
  2. adquirir um valor de variável de um cache, sessão, arquivo, banco de dados, rede ou outra fonte externa.

Observe que não mencionei variáveis globais. Gerenciar o estado com variáveis globais é frequentemente um antipadrão descuidado que torna difícil ou impossível garantir o estado. Variáveis globais em programas complexos devem ser evitadas sempre que possível.

Fluxo de controle

Se um objeto estiver disponível na memória, a iteração será possível e não haverá alteração no fluxo de controle:

js
function getSong() {
  let _song = '';
  let i = 100;
  for (i; i > 0; i -= 1) {
    _song += `${i} beers on the wall, you take one down and pass it around, ${
      i - 1
    } bottles of beer on the wall\n`;
    if (i === 1) {
      _song += "Hey let's get some more beer";
    }
  }
  return _song;
}
function singSong(_song) {
  if (!_song) throw new Error("song is '' empty, FEED ME A SONG!");
  console.log(_song);
}
const song = getSong();
// this will work
singSong(song);

No entanto, se os dados existirem fora da memória, a iteração não funcionará mais:

js
function getSong() {
  let _song = '';
  let i = 100;
  for (i; i > 0; i -= 1) {
    /* eslint-disable no-loop-func */
    setTimeout(function () {
      _song += `${i} beers on the wall, you take one down and pass it around, ${
        i - 1
      } bottles of beer on the wall\n`;
      if (i === 1) {
        _song += "Hey let's get some more beer";
      }
    }, 0);
    /* eslint-enable no-loop-func */
  }
  return _song;
}
function singSong(_song) {
  if (!_song) throw new Error("song is '' empty, FEED ME A SONG!");
  console.log(_song);
}
const song = getSong('beer');
// this will not work
singSong(song);
// Uncaught Error: song is '' empty, FEED ME A SONG!

Por que isso aconteceu? setTimeout instrui a CPU a armazenar as instruções em outro lugar no barramento e instrui que os dados estão programados para serem coletados posteriormente. Milhares de ciclos de CPU se passam antes que a função seja atingida novamente na marca de 0 milissegundos, a CPU busca as instruções do barramento e as executa. O único problema é que a música ('') foi retornada milhares de ciclos antes.

A mesma situação surge no tratamento de sistemas de arquivos e solicitações de rede. A thread principal simplesmente não pode ser bloqueada por um período de tempo indeterminado - portanto, usamos callbacks para programar a execução do código no tempo de maneira controlada.

Você poderá executar quase todas as suas operações com os seguintes 3 padrões:

  1. Em série: as funções serão executadas em uma ordem sequencial estrita, esta é mais semelhante aos loops for.
js
// operations defined elsewhere and ready to execute
const operations = [
  { func: function1, args: args1 },
  { func: function2, args: args2 },
  { func: function3, args: args3 },
];
function executeFunctionWithArgs(operation, callback) {
  // executes function
  const { args, func } = operation;
  func(args, callback);
}
function serialProcedure(operation) {
  if (!operation) process.exit(0); // finished
  executeFunctionWithArgs(operation, function (result) {
    // continue AFTER callback
    serialProcedure(operations.shift());
  });
}
serialProcedure(operations.shift());
  1. Totalmente paralelo: quando a ordenação não é um problema, como enviar e-mail para uma lista de 1.000.000 de destinatários de e-mail.
js
let count = 0;
let success = 0;
const failed = [];
const recipients = [
  { name: 'Bart', email: 'bart@tld' },
  { name: 'Marge', email: 'marge@tld' },
  { name: 'Homer', email: 'homer@tld' },
  { name: 'Lisa', email: 'lisa@tld' },
  { name: 'Maggie', email: 'maggie@tld' },
];
function dispatch(recipient, callback) {
  // `sendEmail` is a hypothetical SMTP client
  sendMail(
    {
      subject: 'Dinner tonight',
      message: 'We have lots of cabbage on the plate. You coming?',
      smtp: recipient.email,
    },
    callback
  );
}
function final(result) {
  console.log(`Result: ${result.count} attempts \
      & ${result.success} succeeded emails`);
  if (result.failed.length)
    console.log(`Failed to send to: \
        \n${result.failed.join('\n')}\n`);
}
recipients.forEach(function (recipient) {
  dispatch(recipient, function (err) {
    if (!err) {
      success += 1;
    } else {
      failed.push(recipient.name);
    }
    count += 1;
    if (count === recipients.length) {
      final({
        count,
        success,
        failed,
      });
    }
  });
});
  1. Paralelo limitado: paralelo com limite, como enviar e-mail com sucesso para 1.000.000 de destinatários de uma lista de 10 milhões de usuários.
js
let successCount = 0;
function final() {
  console.log(`dispatched ${successCount} emails`);
  console.log('finished');
}
function dispatch(recipient, callback) {
  // `sendEmail` is a hypothetical SMTP client
  sendMail(
    {
      subject: 'Dinner tonight',
      message: 'We have lots of cabbage on the plate. You coming?',
      smtp: recipient.email,
    },
    callback
  );
}
function sendOneMillionEmailsOnly() {
  getListOfTenMillionGreatEmails(function (err, bigList) {
    if (err) throw err;
    function serial(recipient) {
      if (!recipient || successCount >= 1000000) return final();
      dispatch(recipient, function (_err) {
        if (!_err) successCount += 1;
        serial(bigList.pop());
      });
    }
    serial(bigList.pop());
  });
}
sendOneMillionEmailsOnly();

Cada um tem seus próprios casos de uso, benefícios e problemas que você pode experimentar e ler com mais detalhes. Mais importante, lembre-se de modularizar suas operações e usar callbacks! Se você sentir alguma dúvida, trate tudo como se fosse middleware!