11

Como bien dice el título, me enfrento a una encrucijada respecto a como utilizar los métodos de recogida de información (un fichero json en una ruta), si hacerlo de manera síncrona o asíncrona.

El problema que me encuentro cuando lo hago de manera asíncrona es que no puedo almacenar nada de manera global hasta que la pila de funciones termine. Con lo que muchas veces recojo variables a null.

Respecto a la manera síncrona, el navegador ya me dice que estás trabajando con cosas obsoletas, que repercutirá en el usuario.

En conclusión. No se qué camino tomar. Si hubiera alguna forma de recoger esa información de manera asíncrona, y en bucle (setInterval), y después guardarla en una variable global solo si huboo algún cambio, sería genial.

Ahora mismo tengo este código para recoger los datos:

//URL para obtener los datos.
var requestURL = "data.geojson";
//Objeto de petición.
var request = new XMLHttpRequest();
//Métodos para realizar una petición de datos a una URL, mediante el método GET y con una cabecera personalizada.
request.open('GET', requestURL, false);
request.send();
if ((request.readyState == 4) && (request.status === 200)) {
    data = request.responseText;
}
Pablo Lozano
  • 45,934
  • 7
  • 48
  • 87
Mr Robot
  • 761
  • 2
  • 6
  • 20

2 Answers2

13

Antes de nada, hay que entender claramente como funciona el modelo de eventos de JavaScript. Esto es fundamental y condiciona totalmente el funcionamiento y la forma de trabajar con el lenguaje. Y es extraordinarimante simple:

  1. El interprete NO HACE NADA hasta que un evento lo fuerce a ello.

    En su estado natural, el lenguaje no está realizando ninguna acción. Se limita a esperar, sin consumir apenas tiempo de CPU, a que un evento suceda.
  2. Toda función JavaScript se ejecuta SIN INTERRUPCIÓN hasta el final.

    Esto incluye posibles llamadas a otras funciones. Toda la secuencia de acciones y llamadas encadenas se produce hasta que se alcanza el return de la función primaria, o bien se produce una excepción (este es el único caso en el que no se alcanza el return de la función primaria).

Para realizar esto, el lenguaje se apoya en 3 elementos:

  1. Funciones callback.
  2. Pila de llamadas.
  3. Cola de eventos.

Funciones callback:

No son sino funciones normales y corrientes, que se pasan como argumento para ser llamadas mas tarde, cuando sea necesario.

En JavaScript, algunos tipos ( números, cadenas ) se pasan por copia; otros, mas complejos ( objetos, arrays ), por referencia. Pues una función callback no es mas que una referencia a una función.

Su uso es el mismo que el de cualquier otro tipo de dato: realizar acciones con ella; pero, si bien en un objeto podemos, por ejemplo, modificar su contenido, la única acción a realizar con una función es llamarla, haciendo nombre( ).

Por ejemplo, en setTimeout( mifuncion, 1000 ), estamos pasando como argumento una callback: la referencia a una función, que será ejecutada en algún momento futuro (aprox. 1 segudo en el futuro).

Aquí entran en juego los cierres (closures en inglés). Para no extendernos, delego en ¿Cómo funcionan las clausuras en JavaScript? ].

Pila:

Es una estructura interna del lenguaje en la que se van apilando las distintas funciones llamadas. Cada vez que, desde una función, se llama a otra, esto se almacena en la pila. Cuando se termina la ejecución de una función, se consulta en la pila a donde tenemos que retornar. El ciclo se repite hasta que la pila esté vacía, lo cual indica que se ha terminado de ejecutar todo el código y el interprete puede descansar hasta que reciba otro evento.

Cola de eventos:

Los eventos se pueden producir en cualquier momento y en cualquier orden. Si el intérprete está ejecutando un código en un momento dado, y se produce un nuevo evento, este se almacena en la cola hasta que llegue su turno de ser analizado y realizar la acción asociada.
Esto se produce de forma invisible para nosotros. Realmente, nuestro código JavaScript no se entera de si se han producido nuevos eventos, hasta que se ejecute la acción asociada.

Falta un último punto por abordar para tener una visión general:

Eventos

Son entes abstractos, manejados internamente por el intérprete. Pueden provenir de distintas fuentes: el Sistema Operativo (ratón, teclado, redimensionado de la ventana, ... ), el reloj ( usados para implementar setInterval( ) y setTimeout( ), generados internamente por el propio intérprete ( DOMContentLoaded, ... ), y/o cualquier otra fuente.

En realidad, a nosotros nos da igual como se implementen y quién los genere. Nuestra única opción es generarlos y responder a ellos. En este último caso, son el pistoletazo de salida, la mecha para la ejecución de nuestro código. Recordemos que, como se dijo al principio, el estado natural del intérprete es no hacer nada.

Una imagen resumen:

introducir la descripción de la imagen aquí

Este modelo, basado en eventos y reacciones a ellos, es muy, muy común en la programación de entornos gráficos, en practicamente cualquier lenguaje.

También, este modelo presenta un inconveniente: el procesado de eventos se detiene hasta que termina la ejecución del código JavaScript actualmente en curso. Ese es el motivo de desaconsejar el uso de funciones síncronas: el navegador no puede hacer nada (salvo seguir guardando eventos en la cola) mientras se está ejecutando el código JavaScript. Y, puesto que dicho código está esperando el retorno desde una función síncrona, no podrá terminar hasta que obtenga el resultado de esa función. Resultado: el navegador se bloquea

A cambio, se usan muy pocos recursos en los tiempos de inactividad (que serán mayoría), y elimina los problemas de acceso concurrente a datos compartidos: no existe el concepto de hilos, y es seguro que 2 funciones no se ejecutarán a la vez.

JavaScript Síncrono

Con lo visto anteriormente, llegamos a la conclusión de que JavaScript es un lenguaje totalmente asíncrono o pasivo: no es posible esperar hasta que pase algo, sino que ese algo, cuando pase, nos avisará.

Existen, no obstante, algunas excepciones. La mas ampliamente conocida: peticiones XMLHttpRequest síncronas.

Los Sistemas Operativos suelen proporcionar 2 formas de acceso a la red: síncronas y asíncronas. En la primera forma, el programa hace una petición y delega en el Sistema Operativo hasta que esta se realiza. Durante ese tiempo, el programa queda dormido. El Sistema Operativo lo marca como en espera, y no pierde el tiempo con él hasta que de por terminada la solicitud. En ese momento, lo marca como listo y continua su ejecución.

Esto tiene sentido en algunos tipos de programas, que, simplemente, no tienen nada que hacer hasta que se complete alguna acción. Al marcarlos como en espera, el Sistema Operativo optimiza los recursos, proporcionando mas tiempo al resto de programas.

Esta forma de trabajar es nefasta en JavaScript. La versión síncrona de XMLHttpRequest, ciertamente, bloquea totalmente la pestaña actual, incluida la gestión de eventos. A efectos prácticos, la pestaña queda muerta hasta que se complete la operación.

La versión síncrona de XMLHttpRequest debe evitarse a toda costa; realmente, en aplicaciones WEB, no hay ningún motivo para usarla.

Volviendo a la pregunta

Aplicando toda la parrafada anterior al problema, el modelo a seguir es muy sencillo: esperamos a obtener los datos, y entonces, realizamos todas las acciones pendientes.

Podemos usar una aproximación clasica:

function obtenerLosDatos( url, callback ) {
  var xhr = new XMLHttpRequest( );

  // Establecemos la REACCIÓN cuando cambie el estado de la solicitud.
  xhr.onreadystatechange( stateChanged );

  // Lanzamos la ACCIÓN.
  xhr.open( 'GET', url );
  xhr.send( );

  // REACCIÓN
  function stateChanged( ) {
    try {
      if( xhr.readyState == 4 ) callback( xhr.state, JSON.parse( xhr.responseText ) );
    } catch( err ) {
      callback( xhr.state, undefined );
    }
  }
}

function PedirDatosYHacerAlgo( ) {
  var url = 'http://example.com/data.json';

  obtenerLosDatos( url, funcionReaccion );

  function funcionReaccion( state, jsonData ) {
    // Recibimos el estado, por lo que podemos comprobar posibles errores.
    if( state != 200 ) {
      alert( 'ERROR !!' );
      return;
    }

    // Comprobamos el JSON recibido.
    if( jsonData === undefined ) {
      alert( 'BAD JSON' );
      return;
    }

    // Aquí tratamos los datos y hacemos lo que tengamos que hacer con ellos.
    ...
  }
}

Casos especiales

Como dijimos, el navegador queda bloqueado mientras se ejecuta nuestro código JavaScript; ante esto, cabe la posibilidad de que, en ciertas ocasiones, tengamos que realizar procesos que tarden mucho tiempo, haciendo que, de cara al usuario, nuestra aplicación se quede pillada. El siguiente código bloquea totalmente la pesataña activa durante un buen rato:

function ProcesoMuyLargo( ) {
  for( let idx = 0; idx < Number.MAX_SAFE_INTEGER; ++idx );
}

ProcesoMuyLargo( );

Para estos casos, JavaScript nos proporciona una muy útil función: setTimeout( ).

setTimeout( exp, time ):
Evalúa la expresión exp pasado un determinado número de milisegundos.

El truco es pasar en time el valor 0. Esto coloca el evento al final de la cola de eventos. Es decir, el navegador procesa los eventos pendientes y, cuando llegue al nuestro, nos devolverá la ejecución. Esto se puede usar para dejar la pestaña receptiva, aunque estemos en procesos muy largos:

function ProcesoMuyLargo( ) {
  var idx = 0;

  function Subproceso( ) {
    for( ; idx < Number.MAX_SAFE_INTEGER; ++idx ) {
      if( !( idx % 10000 ) ) {
        setTimeout( Subproceso, 0 );
        return;
      }
    }
  }
}

ProcesoMuyLargo( );

El número utilizado, 10000, es completamente arbitrario. Cuanto mas grande, menos tardará el bucle en finalizar, pero el navegador se volverá mas vago a las acciones de usuario. Cuanto mas pequeño, el navegador responde antes a las acciones, pero el bucle tardará mas en completarse.

Pablo Lozano
  • 45,934
  • 7
  • 48
  • 87
Trauma
  • 25,297
  • 4
  • 37
  • 60
  • Falta por añadir lo relativo a `async`/`await`. No estoy muy familiarizado con ello ... aun :-) – Trauma Mar 05 '19 at 19:34
  • async y await es más azúcar sintáctico para las promesas – Pablo Lozano Mar 06 '19 at 11:10
  • @PabloLozano No estoy tan seguro ... He hecho mis pruebas, y lo que he observado no se puede reproducir **solo** con promesas ... O yo no soy capaz, al menos O_O – Trauma Mar 06 '19 at 11:23
12

La mejor forma es usar la clase Promise para hacer llamadas asíncronas teniendo la posibilidad de "esperar" a obtener un resultado para ejecutar el código que quieras, incluso si tienes una lista de llamadas las puedes añadir a un array y ejecutar un bloque de código una vez hayan terminado todas con Promise.all(arrayDePromesas).then(... Tienes más información en la documentación.

En tu caso para el código que has puesto puedes utilizar el siguiente método que yo mismo utilizo en mi código:

function request(method, url, body = "") {
    return new Promise(function (resolve, reject) {
        var xhr = new XMLHttpRequest();
        xhr.open(method, url);
        xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
        xhr.onload = function () {
            if (this.status >= 200 && this.status < 300) {
                resolve(xhr.response);
            } else {
                reject({
                    status: this.status,
                    statusText: xhr.statusText
                });
            }
        };
        xhr.onerror = function () {
            reject({
                status: this.status,
                statusText: xhr.statusText
            });
        };

        if(method === "POST"){
            xhr.send(body);
        }
        else{
            xhr.send();
        }
    });
}

y la llamada sería:

request('GET', requestURL)
  .then(function (response) {
    console.log(response);
  });

el then solo se va a ejecutar cuando tienes una respuesta del servidor y se llama al método resolve (al cual le pasas los datos que equivalen a la variable response)

Si quieres usar un intervalo sería de la siguiente manera:

setInterval(function(){ 
    request('GET', requestURL)
      .then(function(response){
        /*comprobar si los datos coinciden*/ 
      });
  }, 15000);
David
  • 1,865
  • 1
  • 7
  • 20
  • La finalidad que busco con obtener datos externos de un json, es aplicar esa información que recojo a un mapa, dibujando elementos en posiciones geográficas concretas, crees que sería útil usar esto? – Mr Robot May 21 '18 at 11:57
  • 1
    no solo útil, sino que creo que es la mejor manera de hacer llamadas en cualquier caso – David May 21 '18 at 14:37
  • Una pregunta más, se trataría de una recogida de información "real time", entonces, ¿qué aplicaría un setInterval a todo el js?, o, ¿a la promesa? – Mr Robot May 21 '18 at 14:56
  • depende de para que quieras utilizar el setInterval, para qué lo quieres usar? – David May 21 '18 at 15:20
  • Pues sería para recoger la información de un json a partir de una url, por ejemplo cada 15 segundos. Y si el json ha variado, actualizar el contenido – Mr Robot May 21 '18 at 19:01
  • 1
    te lo he actualizado en la respuesta para que veas como sería con un intervalo – David May 22 '18 at 08:27