DEV Community

Cover image for Sincronia em aplicações Javascript
Abel Costa
Abel Costa

Posted on

Sincronia em aplicações Javascript

No javascript todas as funções que dependem de execução externa são executadas em background. Assim sendo a forma com que seu código é escrito não necessariamente é a forma com que ele é executado, por isso, mesmo que você escreva um código de foram estruturada, ele não vai executar de cima para baixo, da esquerda para a direita.

O que é uma função assíncrona?

O código Javascript escrito é executado dentro de uma única thread, isso significa que esse código só pode fazer uma única coisa de cada vez a maioria do código javascript que escrevemos é executado de cima para baixo, passo a passo de forma completamente síncrona, é esse sincronismo faz com que quando chamarmos duas funções a execução da segunda só é realizada após a execução da primeira.

O código síncrono espera uma ação ser finalizada antes de partir para a próxima ação.

Porém com a popularização do Ajax a ideia de assincronismo foi popularizada, código assíncrono até o começo do século não era, digamos, popular, a grande maioria das aplicações tinha suas regras de negócio completamente no backend. Muitas gambiarras foram feitas para que houvesse a possibilidade de enviar dados para o backend sem que a página atual fosse recarregada, até que em 2005 surgiu o ajax e [quase] todo mundo começou a gostar da ideia de código assíncrono.

Callbacks

Geralmente se deseja realizar uma tarefa quando uma determinada operação assíncrona é concluída. No começo do ajax tínhamos os callbacks, um callback é nada além de uma função que é passada como argumento de uma outra função, essa outra função é chamada de higher-order function e é executada quando alguma operação é concluída ou quando um evento específico ocorre. Par casos mais simples, os callbacks são extremamente efetivos, o problema surge quando temos de fazer diversas operações assíncronas e ai caímos no callback hell, esse anti-pattern é caracterizado por um código em formato de pirâmide, com diversas funções aninhadas umas dentro das outras.

Image description

Trabalhando com callbacks

Temos um código que tem como objetivo buscar um usuário por id, depois ele tem de buscar o telefone desse usuário e por último tem de buscar o endereço desse usuário, como podemos ver abaixo:

function getUser(id) {
  setTimeout(function () {
    return {
      id: 1,
      name: "Aladin",
      dateOfBirth: new Date()
    }
  }, 1000)
}

function getUserPhone(id) {
  setTimeout(() => {
    return {
      phone: "322234545",
      ddd: "11"
    }
  }, 2000)
}


function getUserAddress(id) {
  setTimeout(() => {
    return {
      address: "Rua voluntários da Pátria",
      number: 90
    }
  }, 3000)
}

const user = getUser();
const phone = getUserPhone(user.id);

console.log(`usuário ${user}`);
console.log(`phone: ${phone}`);
Enter fullscreen mode Exit fullscreen mode

Ao executar o código acima podemos ver que a variável user é undefined isso ocorre primeiramente porque o console.log é executado antes da função de obter usuário getUser. Como já sabemos tudo aquilo que depende de execução externa é executada em background, e a melhor forma de lidar com ela é com um callback.

Vamos supor agora que temos uma lista de usuários, e que temos de obter um determinado usuário dentro dessa lista, podemos então trabalhar com callbacks. Para trabalhar com callbacks temos de fazer duas funções a primeira responsável por realizar a ação que tem de ser realizada, e a segunda responsável por resolver a primeira, é essa que chamamos de callback. Uma função que recebe um callback também pode receber outros parâmetros que podem, ou não, serem repassados ao callback.

Veja o código abaixo, usado para obter um usuário de uma lista:

let users = [
  {
    id: 1,
    name: "Aladin",
    dateOfBirth: new Date()
  },
  {
    id: 2,
    name: "John",
    dateOfBirth: new Date()
  },
  {
    id: 3,
    name: "Maycon",
    dateOfBirth: new Date()
  },
  {
    id: 4,
    name: "Andreas",
    dateOfBirth: new Date()
  },

  {
    id: 5,
    name: "Nicole",
    dateOfBirth: new Date()
  },
];

function getUser(id, cb) {
  setTimeout(() => {
    cb(null, users.filter(user => user.id === id))
  }, 2000)
}

// esse é o callback
function resolveUser (error, user) {
  if (error)
    throw new Error('Failed to get user')

  console.log(user);
}

getUser(3, resolveUser)
Enter fullscreen mode Exit fullscreen mode

A função de callback deve seguir um padrão, este é: o primeiro parâmetro da mesma sempre deve ser um erro, o segundo deve ser o valor a ser resolvido. E a função que deve receber um callback sempre deve o receber por último, quaisquer outros parâmetros devem o anteceder.

Agora vamos supor que temos uma base de dados para trabalhar, onde temos as entidades de usuário, telefone e endereço, e temos de obter todas as informações de um determinado usuário especificado pelo id, teremos um fluxo parecido com este:

let users = [
  {
    id: 1,
    name: "Aladin",
    dateOfBirth: new Date(),
  },
  {
    id: 2,
    name: "John",
    dateOfBirth: new Date(),
  },
  {
    id: 3,
    name: "Maycon",
    dateOfBirth: new Date(),
  },
  {
    id: 4,
    name: "Andreas",
    dateOfBirth: new Date(),
  },

  {
    id: 5,
    name: "Nicole",
    dateOfBirth: new Date(),
  },
];

let addresses = [
  {
    id: 1,
    address: "Rua Voluntários da Pátria, 92",
    userId: 1
  },
  {
    id: 2,
    address: "Rua Azul, 92",
    userId: 2
  },
  {
    id: 3,
    address: "Rua A, 92",
    userId: 3
  },
  {
    id: 4,
    address: "Rua B, 76",
    userId: 4
  },
  {
    id: 5,
    address: "Rua Maracaiba, 32",
    userId: 5
  }
]

let phones = [
  {
    id: 1,
    phone: '32903290',
    userId: 1
  },
  {
    id: 2,
    phone: '90097878',
    userId: 2
  },
  {
    id: 3,
    phone: '21902190',
    userId: 3
  },
  {
    id: 4,
    phone: '77777777',
    userId: 4
  },
  {
    id: 5,
    phone:  '10101010',
    userId: 5
  }
]

function getUser(id, cb) {
  setTimeout(() => {
    cb(null, users.find(user => user.id === id))
  }, 2000)
}

function getPhone(userId, cb) {
  setTimeout(() => {
    cb(null, phones.find(phone => phone.userId === userId))
  }, 2000)
}

function getAddress(userId, cb) {
  setTimeout(() => {
    cb(null, addresses.find(add => add.userId === userId))
  }, 2000)
}

// callback
function resolveUserPhone(error, phone) {
  if (error)
    throw new Error("Failed to get phone number")

  console.log(phone);
}

// outro callback
function resolveUserAddress(error, address) {
  if (error)
    throw new Error("Failed to get address")

  console.log(address);

  getPhone(address.userId, resolveUserPhone)
}

// outro callback
function resolveUser (error, user) {
  if (error)
    throw new Error('Failed to get user')

  console.log(user);

  getAddress(user.id, resolveUserAddress)
}

getUser(3, resolveUser)
Enter fullscreen mode Exit fullscreen mode

Note que no código acima nos resolvemos o problema de sincronização que tínhamos, porém agora temos o problema de aninhamento de código, o nosso código está acoplado. Se a lógica foir mais complexa (se por exemplo adicionarmos mais elementos a serem buscados) lidar com o código será muito difícil.

Promises

A ideia das promises é justamente de representar esses fluxos assíncronos de execução de forma sequencial, resolvendo o problema do callback hell, além disso usar promises favorece bastante o tratamento de exceções.

Promises são como cheques, um cheque é uma forma de um pagador te garantir que vai te pagar um determinado valor no dia estipulado. As promises, representam um valor, que vai ser retornado, em algum momento. Quando a função concluir o processamento necessário para retornar aquilo que foi prometido.

Quando trabalhamos com promises fazemos o uso de dois callbacks para tratar o nosso fluxo:

  • resolve: executado quando a promise foi “cumprida”, ela vai receber como argumento justamente o resultado que vai ser retornado por uma promise. Quando uma promise é resolvida o primeiro .then da cadeia é chamado.
  • reject: executado quando há alguma falha, por exemplo, quando a conexão com a internet cai, ou quando um endpoint está indisponível, ao executá-la você está sinalizando justamente que a operação falhou e que as outras promises que dependem da mesma não podem ser executadas. Quando a promise é rejeitada, então o método .catch é executado

Estados de uma Promise

As promises permitem trabalhar toda a assincronia do Javascript a partir da ideia de estados, os estados de uma promise são:

  • pending: estado inicial, significa que a atividade ainda não foi iniciada, ou ainda não foi rejeitada
  • fulfilled: quando executou todas as operações com sucesso, não podemos manipular o estado de fulfilled, mas podemos passar o valor para ser manipulado por uma função, essa função é o que vai estar dentro de .then
  • rejected: quando a operação falhou, ela vai ser tratada dentro de .catch

Podemos tratar erros em promises tanto com .catch quanto com o segundo parâmetro de .then, usamos o primeiro para tratar erros genéricos e o segundo para tratar erros mais específicos da ação que está sendo realizada.

Refatorando de callbacks para Promises

Para refatorar para Promises temos de mudar um pouco a forma como os nossos métodos foram escritos, mantendo a mesma lógica de buscar um usuário por id.

let users = [
  {
    id: 1,
    name: "Aladin",
    dateOfBirth: new Date(),
  },
  {
    id: 2,
    name: "John",
    dateOfBirth: new Date(),
  },
  {
    id: 3,
    name: "Maycon",
    dateOfBirth: new Date(),
  },
  {
    id: 4,
    name: "Andreas",
    dateOfBirth: new Date(),
  },

  {
    id: 5,
    name: "Nicole",
    dateOfBirth: new Date(),
  },
];

function getUser(id) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      const user = users.find(user => user.id === id);

      if (!user)
        reject(`User not found`)
      else
        resolve(user)
    }, 2000)
  })
}

getUser(3)
  .then(user => console.log(user))
  .catch(err => console.error(err));
Enter fullscreen mode Exit fullscreen mode

Note que o corpo da função anterior foi totalmente movido para dentro da promise, além disso também foi precioso mudar a foram como estávamos buscando pelo usuário dentro da nossa “base de dados”. Mas note como o código fica muito mais legível dessa forma e como podemos tratar melhor os erros que podem vir a ocorrer.

Porém um outro problema ocorre quando temos de lidar com muitos fluxos de dados distintos, se quisermos buscar o endereço e o telefone do usuário, por exemplo. Dentro de um .then é possível passar um dado para que seja usado em um outro .then, assim sendo podemos chamar várias funções que retornam promises de forma encadeada.

let users = [
  {
    id: 1,
    name: "Aladin",
    dateOfBirth: new Date(),
  },
  {
    id: 2,
    name: "John",
    dateOfBirth: new Date(),
  },
  {
    id: 3,
    name: "Maycon",
    dateOfBirth: new Date(),
  },
  {
    id: 4,
    name: "Andreas",
    dateOfBirth: new Date(),
  },

  {
    id: 5,
    name: "Nicole",
    dateOfBirth: new Date(),
  },
];

let addresses = [
  {
    id: 1,
    address: "Rua Voluntários da Pátria, 92",
    userId: 1
  },
  {
    id: 2,
    address: "Rua Azul, 92",
    userId: 2
  },
  {
    id: 3,
    address: "Rua A, 92",
    userId: 3
  },
  {
    id: 4,
    address: "Rua B, 76",
    userId: 4
  },
  {
    id: 5,
    address: "Rua Maracaiba, 32",
    userId: 5
  }
]

let phones = [
  {
    id: 1,
    phone: '32903290',
    userId: 1
  },
  {
    id: 2,
    phone: '90097878',
    userId: 2
  },
  {
    id: 3,
    phone: '21902190',
    userId: 3
  },
  {
    id: 4,
    phone: '77777777',
    userId: 4
  },
  {
    id: 5,
    phone:  '10101010',
    userId: 5
  }
]

function getUser(id) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      const user = users.find(user => user.id === id);

      if (!user)
        reject(`User not found`)
      else
        resolve(user)
    }, 2000)
  })
}

function getPhone(userId) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      const phone = phones.find(phone => phone.userId === userId)

      if (!phone)
        reject(`Phone not found`)
      else  
        resolve(phone)
    }, 2000)
  })
}

function getAddress(userId) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      const address = addresses.find(address => address.userId === userId)

      if (!address)
        reject(`Address not found`)
      else 
        resolve(address)
    }, 3000)
  })
}

getUser(3)
  .then(user => {
    getAddress(user.id)
      .then(address => {
        getPhone(address.userId)
          .then(phone => console.log(user, phone, address))
      })
  })
  .catch(err => console.error(err));
Enter fullscreen mode Exit fullscreen mode

No momento estamos convertendo para promise cada função de callback que temos, mas podemos fazer isso através de um módulo interno do próprio Node, ele nos permite seguir as já estabelecidas convenções de código usando callback, porém em um código que usa promises.

No momento o nosso código com callbacks é escrito da seguinte forma:

let users = [
  {
    id: 1,
    name: "Aladin",
    dateOfBirth: new Date(),
  },
  {
    id: 2,
    name: "John",
    dateOfBirth: new Date(),
  },
  {
    id: 3,
    name: "Maycon",
    dateOfBirth: new Date(),
  },
  {
    id: 4,
    name: "Andreas",
    dateOfBirth: new Date(),
  },

  {
    id: 5,
    name: "Nicole",
    dateOfBirth: new Date(),
  },
];

function getUser(id, callback) {
  setTimeout(() => {
    callback(null, users.find(user => user.id === id))
  }, 3000)
}

function resolveUser(error, user) {
  if (error)
    throw new Error(error)

  console.log(user);
}

getUser(2, resolveUser)
Enter fullscreen mode Exit fullscreen mode

Fazendo o uso da biblioteca util onde temos a possibilidade de usar a função promisify que transforma um método em uma promise, o nosso código fica da seguinte maneira:

const util = require("util"); // import do módulo

let users = [
  {
    id: 1,
    name: "Aladin",
    dateOfBirth: new Date(),
  },
  {
    id: 2,
    name: "John",
    dateOfBirth: new Date(),
  },
  {
    id: 3,
    name: "Maycon",
    dateOfBirth: new Date(),
  },
  {
    id: 4,
    name: "Andreas",
    dateOfBirth: new Date(),
  },

  {
    id: 5,
    name: "Nicole",
    dateOfBirth: new Date(),
  },
];

function getUser(id, callback) {
  setTimeout(() => {
    callback(null, users.find(user => user.id === id))
  }, 3000)
}

const getUserAsync = util.promisify(getUser)

getUserAsync(3)
  .then(user => console.log(user))
  .catch(error => console.log(error))
Enter fullscreen mode Exit fullscreen mode

Async functions

O intuito original das Promises era garantir um código assíncrono que fosse claro e manutenível, as funções async tornaram isso ainda mais presente. Diferentemente da abordagem de migração de callbacks para promises, aqui temos uma forma de escrever o código de maneira mais linear.

Temos no javascript as palavras reservadas async e await, com elas podemos resolver as nossas funções de maneira mais linear, o código dessa forma é quase idêntico ao código de uma função comum, não assíncrona.

O significado

Começando pela palavra async, quando temos uma função marcada com essa palavra significa que temos uma função que sempre retorna uma promise. Porém essa promise já estará resolvida.

async function getOne() {
    return 1
}

console.log(getOne()); // Promise { 1 }
Enter fullscreen mode Exit fullscreen mode

Depois temos a palavra await, sua função é fazer o Javascript esperar até que se tenha uma resposta obtida de um processamento assíncrono.

A utilização delas:

  • Facilita o trabalho com código assíncrono
  • Não altera a performance da aplicação desde que seja usado de forma correta
  • Use apenas quando for necessário tratar a resposta da chamada

A implementação do padrão async/await surgiu primeiramente no typescript, o superset que provém tipagens ao javascript, isso aconteceu porque o typescript é mantido principalmente pela Microsoft, que também mantém o C#, de onde se originou o padrão.

O código de busca por usuário pode ser feito da seguinte forma:

let users = [
  {
    id: 1,
    name: "Aladin",
    dateOfBirth: new Date(),
  },
  {
    id: 2,
    name: "John",
    dateOfBirth: new Date(),
  },
  {
    id: 3,
    name: "Maycon",
    dateOfBirth: new Date(),
  },
  {
    id: 4,
    name: "Andreas",
    dateOfBirth: new Date(),
  },

  {
    id: 5,
    name: "Nicole",
    dateOfBirth: new Date(),
  },
];

let addresses = [
  {
    id: 1,
    address: "Rua Voluntários da Pátria, 92",
    userId: 1
  },
  {
    id: 2,
    address: "Rua Azul, 92",
    userId: 2
  },
  {
    id: 3,
    address: "Rua A, 92",
    userId: 3
  },
  {
    id: 4,
    address: "Rua B, 76",
    userId: 4
  },
  {
    id: 5,
    address: "Rua Maracaiba, 32",
    userId: 5
  }
]

let phones = [
  {
    id: 1,
    phone: '32903290',
    userId: 1
  },
  {
    id: 2,
    phone: '90097878',
    userId: 2
  },
  {
    id: 3,
    phone: '21902190',
    userId: 3
  },
  {
    id: 4,
    phone: '77777777',
    userId: 4
  },
  {
    id: 5,
    phone:  '10101010',
    userId: 5
  }
]

// retorna uma promise
function getUserById(id) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      const user = users.find(user => user.id === id);

      if (!user)
        reject("User not found")
      else 
        resolve(user)

    }, 5000);
  });
}

// retorna uma promise
function getAddressByUserId(id) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      const address = addresses.find(address => address.userId === id);

      if (!address)
        reject("Address not found")
      else
        resolve(address)

    }, 5000)
  });
} 

// retorna uma promise
function getPhoneByUserId(id) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      const phone = phones.find(phone => phone.userId === id);

      if (!phone)
        reject("Phone not found")
      else 
        resolve(phone)

    }, 5000)
  });
}

async function main(id) {
  try {
    const user = await getUserById(id);
    const phone = await getPhoneByUserId(user.id);
    const address = await getAddressByUserId(user.id);

    console.log(`
      Nome: ${user.name},
      Telefone: ${phone.phone},
      Endereço: ${address.address}
    `);
  } catch (error) {
    console.log(error);
  }
}

main(2);
Enter fullscreen mode Exit fullscreen mode

Note que estamos lidando apenas com funções que retornam promises, entretanto ainda temos um problema, o código acima se executado leva muito tempo para terminar. Podemos mensurar o tempo que está sendo utilizando na execução do código usando console.time, como podemos ver abaixo:

async function main(id) {
  try {
    console.time('promises-execution')
    const user = await getUserById(id);
    const phone = await getPhoneByUserId(user.id);
    const address = await getAddressByUserId(user.id);

    console.log(`
      Nome: ${user.name},
      Telefone: ${phone.phone},
      Endereço: ${address.address}
    `);
    console.timeEnd('promises-execution')
  } catch (error) {
    console.log(error);
  }
}
Enter fullscreen mode Exit fullscreen mode

Vamos ter como resposta:

node index.js

      Nome: John,
      Telefone: 90097878,
      Endereço: Rua Azul, 92

promises-execution: 15.032s
Enter fullscreen mode Exit fullscreen mode

Tempo de execução de 15 segundos...

Esse tempo de execução está muito grande pelo fato de que temos de lidar com várias chamadas assíncronas que independem umas das outras, assim sendo, podemos fazer o uso de Promise.all, ele recebe como parâmetro um array de promises e retorna uma única promise que ao ser resolvida retorna um array de resultados das promises colocadas no parâmetro, em ordem. Essa promise vai ser resolvida quando todas as promises dentro do array forem resolvidas. E vai ser rejeitada imediatamente no momento em que uma das promises dentro do array é rejeitada.

Esse método pode ser muito útil quando temos de lidar com várias promises, é tipicamente usado quando existem muitas atividades assíncronas que o código em geral depende de todas para funcionar com sucesso. Isso nos permite reduzir drasticamente o tempo de execução das nossas promises.

async function main(id) {
  try {
    console.time('promises-execution')

    const result = await Promise.all([
      getUserById(id),
      getPhoneByUserId(id),
      getAddressByUserId(id)
    ]);

    console.log(`
      Nome: ${result[0].name},
      Telefone: ${result[1].phone},
      Endereço: ${result[2].address}
    `);

    console.timeEnd('promises-execution')
  } catch (error) {
    console.log(error);
  }
}
Enter fullscreen mode Exit fullscreen mode

Se verificarmos o tempo de execução desse código temos que é de 5 segundos.

Porque esse tempo reduziu tanto? Simples, porque dentro de Promise.all as promises são processadas paralelamente, então antes tínhamos 3 promises de 5 segundos, onde cada uma delas era resolvida de forma sequencial, então o tempo de execução vai ser a soma da execução das três promises individualmente, agora temos apenas 5 segundos de execução porque todas são executadas dentro desses 5 segundos.

Caso as promises dentro do array do parâmetro tenham tempos distintos de execução, o tempo de execução de Promise.all recai sobre o worst case scenario, ou seja, o tempo total de execução vai ser o maior tempo de execução de uma promise individual.

TL;DR

Existem três abordagens distintas de se lidar com código assíncrono em Javascript:

  1. callbacks: convenção mais antiga usada na comunidade, principalmente graças ao ajax, é uma abordagem que pode ser usada para resolver pequenos problemas mas no geral é vista como antiquada por gerar um código muito aninhado (callback hell)
  2. promises: resolve o problema de código aninhado criado com os callbacks, porém ainda temos a dificuldade de um código não linear e que nem sempre é claro de ser lido, além de dificultar o tratamento de erros
  3. async/await: trata o problema das promises de fomra mais linear se comparado as outras duas opções, permite um código mais flat e um melhor tratamento de erros, porém se usado de forma inadequada pode resultar em um impacto negativo da performance da aplicação.

bibliografia:

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/all

https://medium.com/@alcidesqueiroz/javascript-assíncrono-callbacks-promises-e-async-functions-9191b8272298

https://javascript.info/async-await

https://erickwendel.teachable.com/courses/448292/lectures/6939052

https://erickwendel.teachable.com/courses/448292/lectures/6939053

https://erickwendel.teachable.com/courses/448292/lectures/6939055

Top comments (0)