Skip to content

06. Asincronismo

Carlos Jaramillo edited this page Jun 5, 2021 · 17 revisions

Test Javascript en su nucleo es sincrono y bloqueante, sin embargo a partir de ES6+ se introdujeron características asíncronas al core del lenguaje, volviendolo un lenguaje asíncrono y no bloqueante. El manejador de eventos es el eventloop, está implementado en un unico hilo para sus interfaces de entrada y de salida. Asincronismo es una acción que no ocurre al mismo tiempo. Pero cómo javascript sólo tiene 1 hilo, No podemos disparar o recibir multiples cosas.

Asincronismo

JavaScript sólo puede hacer una tarea a la vez (No es multi-tarea), sin embargo; es capaz de delegar la ejecución de ciertas funciones a otros procesos. Este modelo de concurrencia se llama EventLoop.

Pila de ejecución o Call Stack, se ponen las llamadas a funciones según el orden de ejecución del programa (se saca de la pila al terminar de ejecutar cada función)

Callback, una función que se ejecutará cuando regrese la respuesta del servidor. Javascript sigue ejecutando el programa principal, y cuando llegue la respuesta la funcion a ejecutar va a parar a la cola de tareas (se encolan, segun orden de llegada) Aquí llegan:

  • las peticiones a servidores
  • interacciones visuales
  • navegación client-side
  • eventos que se realizan cada cierto tiempo.

Javascript ejecuta primero lo sincrono (pila de tareas), y posteriormente lo asincrono (cola de tareas).

console.log('a');
// Envía la función a la cola de tareas, mientras sigue ejecutando el programa principal
setTimeout(() => console.log('b'), 0);
console.log('c');
// Cuando termina la ejecución del programa inicial, comienza a ejecutar las tareas de la cola de tareas

Callbacks

Un callback es una función que se pasa a otra función como un argumento. Con la expectativa de que esta sea ejecutada. Usualmente esta función se invoca, después, dentro de la función externa para completar alguna acción. Es la manera en la que Javascript ha implementado el asincronismo.

// callback example
function sum(num1, num2) {
  return num1 + num2;
}

function calc(num1, num2, callback) {
  return callback(num1, num2)
}

calc(1,2,sum);

// callback example
function date(callback) {
  console.log(new Date);
  setTimeout(() => {
    let date = new Date;
    callback(date);
  }, 3000)
}

function printDate(dateNow) {
   console.log(dateNow);
}

date(printDate);

// callback example
const URL = 'https://swapi.co/api';
const PEOPLE_URL = '/people/:id';

const LUKE_URL = `${URL}${PEOPLE_URL.replace(':id',1)}`;

fetch(LUKE_URL)
.then((response)=>response.json())
.then((persona)=>{
    console.log(`Hola, yo soy ${persona.name}`)
})
.catch( error=> console.error('Error: ', error))
// no es necesario importarlo en el browser, porque ellos ya lo traen.
let XMLHttpRequest = require('xmlhttprequest').XMLHttpRequest; // creado by Microsoft y adaptado como estándar en las empresas
let api = 'https://rickandmortyapi.com/api/character/';

function fetchData(url_api, callback) {
  let xhttp = new XMLHttpRequest();
  // el tercer valor, por defecto está en true, es para activar el asincronismo dentro de xmlhttprequest
  xhttp.open('GET', url_api, true); 
  xhttp.onreadystatechange = function (event) {
    if(xhttp.readyState === 4) { // validar ultimo estado de la peticion
      if(xhttp.status === 200) { // validar status de la petición
        callback(null, JSON.parse(xhttp.responseText))
      } else {
        const error = new Error('Error ' + url_api);
        return callback(error, null);
      }
    }
  }
  xhttp.send();
}


fetchData(API, (error1, data1) => { // Lista de personajes
  if(error1) return console.error(error1);
  
  fetchData(api + data1.results[0].id, (error2, data2) => { // Petición a personaje
    if(error2) return console.error(error2);

    fetchData(data2.origin.url, (error3, data3) => { // api que contiene origen del personaje
      if(error3) return console.error(error3);
      
      // mostramos los resultados :) 
      console.log(data1.info.count);
      console.log(data2.name);
      console.log(data3.dimension);
      
      // rutas de las peticiones en orden
      console.log(api);
      console.log(api + data1.results[0].id); 
      console.log(data2.origin.url); 
    });
  });
});

Multiples request

Las respuestas en js no nos llegan en el mismo orden, por el asincronismo. A no ser que usemos callbacks

const URL = 'https://swapi.co/api';
const PEOPLE_URL = '/people/:id';

const getPersona = (personaId, callback) => {
    const LUKE_URL = `${URL}${PEOPLE_URL.replace(':id',personaId)}`;
    fetch(LUKE_URL)
    .then((response)=>response.json())
    .then((persona)=>{
        console.log(`Hola, yo soy ${persona.name}`)
        if (callback) {
            callback();
        }
    })
    .catch( error=> console.error('Error: ', error))  // manejo de error en callback
};

/*
// No llegan en el mismo orden 

getPersona(1);
getPersona(2);
getPersona(3);
*/

// Callback Hell
getPersona(1, function () {
    getPersona(2, function () {
        getPersona(3);
    });
});

Promesas

Función no-bloqueante y asincrona la cual puede retornar un valor ahora, en el futuro o nunca.

Son valores que aun no conocemos, la promesa de que ahí habrá un valor cuando una acción asincrona/sincrona suceda y se resuelva. Son un obj de JS

Anteriormente usabamos bluebird cuando no todos los browsers tenian soporte, ahora se pueden usar polyfills para browsers que no tengan esta clase creada aun.

new Promise(function(resolve, reject){

}).then(valor => {
    // aquí se puede retornar otra promesa, para ir encadenandolas en acciones sucesivas asincronas
}).catch(err => {

});


Promise.reject(); // static, create a promise with rejected status

Estados:

  • Pending (Cuando se crea, inicializa)
  • Fullfilled (resolve) (Cuando se resuelve, exitosamente)
 .then(val => ..) // recibe una función
  • Rejected (Error)
.catch(err => ..)  // recibe una función
  • Setled (Finalziada con exito o error)
const URL = 'https://swapi.co/api';
const PEOPLE_URL = '/people/:id';

const HEADERS = new Headers();
var miInit = { method: 'GET',
  headers: HEADERS,
   mode: 'cors',
  cache: 'default'
};

const getPersona = async (personaId) => {
    const LUKE_URL = `${URL}${PEOPLE_URL.replace(':id',personaId)}`;
    // function(resolve, reject)
    return fetch(LUKE_URL, miInit)
        .then(response => response.json())
        .catch(error => console.log(`Error al obtener el personaje ${id}`)) 
};

const luke = await getPersona(1)
console.log(luke.name);
console.log('Sacha');

Promesas encadenadas

A diferencia de los callbacks que son anidados, las promesas son encadenadas.

getPersona(1)
    .then(persona => {
        console.log(`Hola, yo soy ${persona.name}`)
        return getPersona(2)
    })
    .then(persona => {
        console.log(`Hola, yo soy ${persona.name}`)
        return getPersona(3)
    })
    .then(persona => {
        console.log(`Hola, yo soy ${persona.name}`)
    })
    .catch(`Error`);

Multiples promesas en paralelo

var ids = [1, 2, 3, 4, 5, 6,7];
var promesas = ids.map(id => getPersona(id));

Promise
    .all(promesas)
    .then(personajes => console.log(personajes))
    .catch(`Error`);
const fetchData = (url_api) => {
    return new Promise((resolve,reject) => {
      const xhttp = new XMLHttpRequest();
      xhttp.open('GET', url_api, true);
      xhttp.onreadystatechange = (() => {
        if(xhttp.readyState === 4) {
          (xhttp.status === 200) 
            ? resolve(JSON.parse(xhttp.responseText)) : reject(new Error('Error ', url_api))
        }
      })   
      xhttp.send()
    })
}

const API = "https://rickandmortyapi.com/api/character/";

fetchData(API)
    .then(data => {
        console.log(data.info.count)
        return fetchData(`${API}${data.results[0].id}`)
    })
    .then(data => {
        debugger
        console.log(data.name)
        return fetchData(data.origin.url)
    })
    .then(data => {
        console.log(data.dimension)
    })
    .catch(err => console.error(err))

Async-await

La finalidad de las funciones async/await es simplificar el comportamiento del uso síncrono de promesas y realizar algún comportamiento específico en un grupo de Promise. Del mismo modo que las Promesas son semejantes a las devoluciones de llamadas estructuradas, async/await se asemejan a una combinación de generadores y promesas.

Se marca la función al inicio como async, se pone la parte asincrona en un try-catch y se le marca como await. Siempre que se marca una función como async, esta retornará una promesa (de manera implicita).

async function obtenerPersonajes() {
    var ids = [1, 2, 3, 4, 5, 6,7];
    var promesas = ids.map(id => getPersona(id));
    try {
        var personajes = await Promise.all(promesas);
        console.log(personajes)
    } catch (id) {
        console.log(`error in ${id}`);
    }
}

obtenerPersonajes();
const apiKey = "b89fc45c2067cbd33560270639722eae";

async function getMovie(id) {
  const url = `https://api.themoviedb.org/3/movie/${id}?api_key=${apiKey}`;
  const response = await fetch(url);
  const data = await response.json();
  return data;
  // return fetch(url).then(reponse => response.json());
}

async function getPopularMovies() {
  const url = `https://api.themoviedb.org/3/discover/movie?sort_by=popularity.desc&api_key=${apiKey}`;
  const response = await fetch(url);
  const data = await response.json();
  return data.results;
  // return fetch(url).then(reponse => response.json()).then(data => data.results);
}

async function getTopMoviesIds(n = 3) {
/*
  return getPopularMovies().then(popularMovies =>
    popularMovies.slice(0, n).map(movie => movie.id)
  );
*/
/*
  try {
    const popularMovies = await getPopularMovies();
  } catch (error) {
    console.log(error.message)
  }
*/
  const popularMovies = await getPopularMovies();
  const ids = popularMovies.slice(0, n).map(movie => movie.id);
  return ids;
}

async function getTopMoviesInSequence() {
  const ids = await getTopMoviesIds();
  const movies = [];

  for (const id of ids) {
    const movie = await getMovie(id);
    movies.push(movie);
  }

  return movies;
}

async function getTopMoviesInParallel() {
  const ids = await getTopMoviesIds();
  const moviePromises = ids.map(id => getMovie(id));

  const movies = await Promise.all(moviePromises);

  return movies;
}

async function getFastestTopMovie() {
  const ids = await getTopMoviesIds();
  const moviePromises = ids.map(id => getMovie(id));

  const movie = await Promise.race(moviePromises);
  return movie;
}

function renderMovies(movies) {
    const movieList = document.getElementById('movies');
    movieList.innerHTML = '';

    movies.forEach(movie => {
      const listItem = document.createElement('li');
      listItem.innerHTML = `
        <img src="https://image.tmdb.org/t/p/w342${movie.poster_path}" />
        <h5>${movie.title}</h5>
        <p>Released on <em>${movie.release_date}</em></p>
        `;

      movieList.appendChild(listItem);
    });
};

document.getElementById('fastest').onclick = async function() {
    const movie = await getFastestTopMovie();
    renderMovies([movie]);
};
const doSomethingAsync = () => {
    return newPromise ((resolve, reject) => {
        (true)
            ? setTimeout(() => resolve('Do Something Async'), 3000)
            : reject (newError('Test Error'))
    });
}

const doSomething = async () => {
    const something = await doSomethingAsync();
    console.log(something);
}

console.log('Before');
doSomething();
console.log('After');

const anotherFunction = async () => {
    try {
        const something = await doSomethingAsync();
        console.log(something);
        
    } catch (error){
        console.error(error);
    }
}

console.log('Before');
anotherFunction();
console.log('After');

const API = "https://rickandmortyapi.com/api/character/";

const anotherFunction = async (url_api) => {
    try {
        const data = await fetchData(url_api)
        const character = await fetchData(`${API}${data.results[0].id}`)
        const origin = await fetchData(character.origin.url)

        console.log(data.info.count)
        console.log(character.name)
        console.log(origin.dimension)
    } catch (error) {
        console.error(error)
    }
}

console.log('Before')
anotherFunction(API)
console.log('After')

Detener peticiones fetch

Con fetch tenemos algo llamado AbortController que nos permite enviar una señal a una petición en plena ejecución para detenerla.

const img = document.getElementById("huge-image");
const loadButton = document.getElementById("load");
const stopButton = document.getElementById("stop");

const url = "https://images.pexels.com/photos/974470/nature-stars-milky-way-galaxy-974470.jpeg?q=100";
let controller;

function startLoading() {
  loadButton.disabled = true;
  // Cambia el texto de su contenido
  loadButton.innerText = "Loading...";
  stopButton.disabled = false;
}
function stopLoading() {
  loadButton.disabled = false;
  loadButton.innerText = "Load HUGE Image";
  stopButton.disabled = true;
}

loadButton.onclick = async function() {
  startLoading();
  controller = new AbortController();
  try {
    const response = await fetch(url, { signal: controller.signal });
    // Obtener el binario de la imagen
    const blob = await response.blob();
    // blob-binario a URL
    const imgUrl = URL.createObjectURL(blob);
    // Ahora el src se lo asignamos a la url de la imagen
    img.src = imgUrl;
  } catch (error) {
    console.log(error.message);
  }
  stopLoading();
};

stopButton.onclick = function() {
  controller.abort();
  stopLoading();
};

Diferencias Callbacks, Promesas, Async / Await

Callback

Ventajas

Cuando una función recibe otra función significa que va a ejecutarse sin ningún problema. Son universales, corren en cualquier navegador. Nos da garantia que se va a correr en cualquier lado

Desventajas

La composición es tosca, ya que se anida una función dentro de otra llega a ser compleja y se llega a un Callback Hell lo cual no podría ser soportada para el desarrollador ya que nose puede entender. Flujo poco intuitivo. No podemos manejar una excepción

Promesas

Ventajas

  • Facilmente enlazables con los then y return
  • Es mas intituitivo
  • Es mas facil de leer
  • Es poderoso, nos permite tener una gran capacidad de trabajar con asincronismo

Desventaja

  • NO maneja excepciones
  • Podemos ser propensos a errores si no retornamos el siguiente llamado
  • Requiere un polyfill para funcionar en todos los navegadores se tiene que transpilar el código
  • con una herramienta para que pueda correr en todos los navegadores

Async Await

Ventajas

Se usa el try/catch y se pueden manejar las excepciones y poder trabajar de manera fluida en la construcción del programa. Se pueden leer mejor

Desventajas

  • Se tiene que esperar por cada uno de los llamados
  • Si se quieren hacer otros llamados hay que esperar que los await se ejecuten
  • Requiere Polyfill como las promesas