Skip to content

Latest commit

 

History

History
272 lines (214 loc) · 8.87 KB

49.controle_de_fluxo_assincrono.md

File metadata and controls

272 lines (214 loc) · 8.87 KB

Controle de fluxo assíncrono

Índice

O material neste post é fortemente inspirado no livro Node.js da Mixu.

Em sua essência, o JavaScript foi projetado para não bloquear o encadeamento "principal", é onde as visualizações são renderizadas. Você pode imaginar a importância disso no navegador. Quando o encadeamento principal é bloqueado, isso 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 difíceis de lidar em procedimentos mais complicados. Isso geralmente resulta em "inferno de callbacks", onde várias funções aninhadas com callback tornam o código mais desafiador para ler, depurar, organizar etc.

async1(function (input, result1) {
  async2(function (result2) {
    async3(function (result3) {
      async4(function (result4) {
        async5(function (output) {
          // faz alguma coisa com a saida
        });
      });
    });
  });
});

É claro que, na vida real, provavelmente haveria linhas de código adicionais para lidar com result1, result2, etc., portanto, o tamanho e a complexida desse problema geralmente resultam em um código que parece muito mais confuso do que o exemplo acima.

É aqui que as funções entram em grande uso. Operações mais complexos são compostas de muito de muitas funções:

  • inicializador de style/input
  • middleware
  • terminator

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

  • variáveis em um ambiente global
  • invocação direta com ou sem argumentos
  • valores obtidos pelo sistema de arquivos ou solicitações de rede

As solicitações de rede podem ser solicitações de entrada iniciadas por uma rede estrangeira, por outro aplicativo na mesma rede ou pelo próprio aplicativo na mesma rede ou em uma rede estrangeira.

Uma função de middleware retornará outra função e uma função terminator invocará o callback. O seguinte ilustra o fluxo para solicitações de rede.

function final(someInput, callback) {
  callback(`${someInput} and terminated by executing callback `);
}

function middleware(someInput, callback) {
  return final(`${someInput} touched by middleware `, callback);
}

function initiate() {
  const someInput = 'hello this is a function ';
  middleware(function (result) {
    console.log(result);
    // requer o callback para retornar o resultado
  });
}

initiate();

Gerenciamento de estado

As funções podem ou não ser dependentes 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.

Desta forma, existem duas estratégias principais para gestão do estado:

  1. passando 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 eu não mencionei a variável global. Gerenciar o estado com variável globais geralmente é um anti-pattern desleixado que torna difícil ou impossível garantir o estado. Variáveis globais em programas complexos devem ser evitadas quando possível.

Fluxo de controle

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

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();
// isso vai funcionar
singSong(song);

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

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');
// isso vai funcionar
singSong(song);
// Uncaught Error: song is '' empty, FEED ME A SONG!

Por quê isso aconteceu? setTimeout instrui a CPU a armazenar as instruções em outro lugar no barramento e instrui que os dados estão programadores para serem coletados posteriormente. Milhares de ciclos de CPU passam antes que a função atinja novamente a marca de 0 milessegundos, 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 ao lidar com 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 agendar a execução do código no tempo de maneira controlada.

Você poderá realizar quase todas as suas operações com os 3 padrões a seguir:

  1. Em série: as funções serão executadas em uma ordem sequencial estrita, esta é mais semelhante aos loops for.

    // operações definidas em outro lugar e prontas para execução.
    const operations = [
    { func: function1, args: args1 },
    { func: function2, args: args2 },
    { func: function3, args: args3 },
    ];
    
    function executeFunctionWithArgs(operation, callback) {
     // executa a função
     const { args, func } = operation;
     func(args, callback);
    }
    
    function serialProcedure(operation) {
     if (!operation) process.exit(0); // finalizado
     executeFunctionWithArgs(operation, function (result) {
     // continua DEPOIS da callback
     serialProcedure(operations.shift());
     });
    }
    
    serialProcedure(operations.shift());
    
    
  2. Paralelo total: quando o pedido não é um problema, como enviar por e-mail uma lista de 1.000.00 destinatários de e-mail.

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` é um cliente SMTP hipoteticamente
  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`);
}

items.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. Parelelo limitado: parelelo com limite, como enviar e-mail com sucesso para 1.000.000 destinatários de uma lista de usuários 10E7.
let successCount = 0;

function final() {
  console.log(`dispatched ${successCount} emails`);
  console.log('finished');
}

function dispatch(recipient, callback) {
  // `sendEmail` é um cliente SMTP hipoteticamente
  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.shift());
      });
    }

    serial(bigList.shift());
  });
}

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 um middleware!