1. Code
  2. Coding Fundamentals

Un manual sobre funciones ES7 Async

Si ha estado siguiendo el mundo de JavaScript, es probable que haya oído hablar de promesas. Hay algunos excelentes tutoriales en línea si desea conocer las promesas, pero no las explicaré aquí; este artículo asume que ya tienes un conocimiento práctico de las promesas.
Scroll to top

Spanish (Español) translation by Elías Nicolás (you can also view the original English article)

Si ha estado siguiendo el mundo de JavaScript, es probable que haya oído hablar de promesas. Hay algunos excelentes tutoriales en línea si desea conocer las promesas, pero no las explicaré aquí; este artículo asume que ya tienes un conocimiento práctico de las promesas.

Las promesas se promocionan como el futuro de la programación asincrónica en JavaScript. Las promesas realmente son geniales y ayudan a resolver una gran cantidad de problemas que surgen con la programación asincrónica, pero esa afirmación es solo algo correcta. En realidad, las promesas son la base del futuro de la programación asincrónica en JavaScript. Idealmente, las promesas se esconderán entre bastidores y podremos escribir nuestro código asíncrono como si fuera síncrono.

En ECMAScript 7, esto se convertirá en algo más que un sueño fantasioso: se convertirá en realidad, y te mostraré esa realidad, llamada funciones asíncronas, en este momento. ¿Por qué estamos hablando de esto ahora? Después de todo, ES6 no ha sido completamente finalizado, así que quién sabe cuánto tiempo pasará antes de que veamos ES7. La verdad es que puedes usar esta tecnología ahora mismo, y al final de esta publicación, te mostraré cómo hacerlo.

El estado actual de los asuntos

Antes de comenzar a demostrar cómo usar las funciones de sincronización, quiero ir a través de algunos ejemplos con promesas (utilizando las promesas de ES6). Más tarde, convertiré estos ejemplos para utilizar las funciones asíncronas para que pueda ver la gran diferencia que hace.

Ejemplos

Para nuestro primer ejemplo, haremos algo realmente simple: llamar a una función asíncrona y registrar el valor que devuelve.

1
function getValues() {
2
    return Promise.resolve([1,2,3,4]);
3
}
4
5
getValues().then(function(values) {
6
    console.log(values);
7
});

Ahora que tenemos definido ese ejemplo básico, vayamos a algo un poco más complicado. Usaré y modificaré ejemplos de una publicación en mi propio blog que revisa algunos patrones para usar promesas en diferentes escenarios. Cada uno de los ejemplos recupera asíncronamente una matriz de valores, realiza una operación asincrónica que transforma cada valor en la matriz, registra cada nuevo valor y finalmente devuelve la matriz llena con los nuevos valores.

Primero, veremos un ejemplo que ejecutará varias operaciones asíncronas en paralelo, y luego responderá a ellas inmediatamente cuando termine cada una, independientemente del orden en que terminen. La función getValues es la misma del ejemplo anterior. La función asyncOperation también se reutilizará en los próximos ejemplos.

1
function asyncOperation(value) {
2
    return Promise.resolve(value + 1);
3
}
4
5
function foo() {
6
    return getValues().then(function(values) {
7
        var operations = values.map(function(value) {
8
            return asyncOperation(value).then(function(newValue) {
9
                console.log(newValue);
10
                return newValue;
11
            });
12
        });
13
 
14
        return Promise.all(operations);
15
    }).catch(function(err) {
16
        console.log('We had an ', err);
17
    });
18
}

Podemos hacer exactamente lo mismo, pero asegúrese de que el registro ocurra en el orden de los elementos en la matriz. En otras palabras, este próximo ejemplo hará el trabajo asíncrono en paralelo, pero el trabajo síncrono será secuencial:

1
function foo() {
2
    return getValues().then(function(values) {
3
        var operations = values.map(asyncOperation);
4
       
5
        return Promise.all(operations).then(function(newValues) {
6
            newValues.forEach(function(newValue) {
7
                console.log(newValue);
8
            });
9
 
10
            return newValues;
11
        });
12
    }).catch(function(err) {
13
        console.log('We had an ', err);
14
    });
15
}

Nuestro último ejemplo demostrará un patrón donde esperamos que finalice una operación asíncrona anterior antes de comenzar la siguiente. No hay nada corriendo en paralelo en este ejemplo; todo es secuencial

1
function foo() {
2
    var newValues = [];
3
    return getValues().then(function(values) {
4
        return values.reduce(function(previousOperation, value) {
5
            return previousOperation.then(function() {
6
                return asyncOperation(value);
7
            }).then(function(newValue) {
8
                console.log(newValue);
9
                newValues.push(newValue);
10
            });
11
        }, Promise.resolve()).then(function() {
12
        return newValues;
13
        });
14
    }).catch(function(err) {
15
        console.log('We had an ', err);
16
    });
17
}

Incluso con la capacidad de promesas de reducir la anulación de devolución de llamadas, realmente no ayuda mucho. Ejecutar una cantidad desconocida de llamadas asincrónicas secuenciales será complicado sin importar lo que haga. Es especialmente espantoso ver todas esas palabras clave return anidadas. Si pasamos la matriz de valores newValues a través de las promesas en la devolución de llamada reduce reducción en lugar de hacerlo global para toda la función foo, tendríamos que ajustar el código para tener aún más retornos anidados, como este:

1
function foo() {
2
    return getValues().then(function(values) {
3
        return values.reduce(function(previousOperation, value) {
4
            return previousOperation.then(function(newValues) {
5
                return asyncOperation(value).then(function(newValue) {
6
                    console.log(newValue);
7
                    newValues.push(newValue);
8
                    return newValues;
9
                });
10
            });
11
        }, Promise.resolve([]));
12
    }).catch(function(err) {
13
        console.log('We had an ', err);
14
    });
15
}

¿No estás de acuerdo en que tenemos que arreglar esto? Veamos la solución.

Funciones asíncronas para el rescate

Incluso con promesas, la programación asincrónica no es exactamente simple y no siempre fluye muy bien de la A a la Z. La programación sincrónica es mucho más simple y está escrita y se lee con mucha más naturalidad. La especificación de Funciones Async busca en un medio (usando generadores ES6 de fondo) de escribir su código como si fuera síncrono.

¿Cómo los usamos?

Lo primero que debemos hacer es prefijar nuestras funciones con la palabra clave async. Sin esta palabra clave en su lugar, no podemos usar la importantísima palabra clave await dentro de esa función, que explicaré en un momento.

La palabra clave async no solo nos permite usar await, también asegura que la función devolverá un objeto Promise. Dentro de una función asíncrona, cada vez que return un valor, la función realmente devolverá una Promise que se resuelve con ese valor. La forma de rechazar es arrojar un error, en cuyo caso el valor de rechazo será el objeto de error. Aquí hay un ejemplo simple:

1
async function foo() {
2
    if( Math.round(Math.random()) )
3
        return 'Success!';
4
    else
5
        throw 'Failure!';
6
}
7
8
// Is equivalent to...

9
10
function foo() {
11
    if( Math.round(Math.random()) )
12
        return Promise.resolve('Success!');
13
    else
14
        return Promise.reject('Failure!');
15
}

Ni siquiera hemos llegado a la mejor parte y ya hemos hecho que nuestro código se parezca más al código sincrónico porque pudimos dejar de jugar explícitamente con el objeto Promise. Podemos tomar cualquier función y hacer que devuelva un objeto Promise simplemente agregando la palabra clave async en frente.

Avancemos y convierta nuestras funciones getValues y asyncOperation:

1
async function getValues() {
2
    return [1,2,3,4];
3
}
4
5
async function asyncOperation(value) {
6
    return value + 1;
7
}

¡Fácil! Ahora, echemos un vistazo a la mejor parte de todas: la palabra clave await. Dentro de su función asíncrona, cada vez que realiza una operación que devuelve una promesa, puede lanzar la palabra clave await delante de ella, y dejará de ejecutar el resto de la función hasta que la promesa devuelta se haya resuelto o rechazado. En ese momento, la await promisingOperation() evaluará el valor resuelto o rechazado. Por ejemplo:

1
function promisingOperation() {
2
    return new Promise(function(resolve, reject) {
3
        setTimeout(function() {
4
            if( Math.round(Math.random()) )
5
                resolve('Success!');
6
            else
7
                reject('Failure!');
8
        }, 1000);
9
    }
10
}
11
12
async function foo() {
13
    var message = await promisingOperation();
14
    console.log(message);
15
}

Cuando llame a foo, esperará hasta que se resuelva promisingOperation y luego cerrará sesión en "¡éxito!" mensaje, o promisingOperation rechazará, en cuyo caso el rechazo se pasará y foo rechazará con "¡Fallo!". Como foo no devuelve nada, se resolverá con undefined suponiendo que la promisingOperation es exitosa.

Solo queda una pregunta: ¿cómo resolvemos los fallos? La respuesta a esa pregunta es simple: todo lo que tenemos que hacer es envolverlo en un try...catch bloque. Si una de las operaciones asíncronas es rechazada, podemos catch y manejarla:

1
async function foo() {
2
    try {
3
        var message = await promisingOperation();
4
        console.log(message);
5
    } catch (e) {
6
        console.log('We failed:', e);
7
    }
8
}

Ahora que hemos encontrado todos los elementos básicos, repasemos nuestros ejemplos de promesas anteriores y conviértelos para usar funciones asíncronas.

Ejemplos

El primer ejemplo anterior creó getValues y lo usó. Ya hemos vuelto a crear getValues, así que solo tenemos que volver a crear el código para usarlo. Hay una advertencia potencial para las funciones asíncronas que se muestra aquí: se requiere que el código esté en una función. El ejemplo anterior estaba en el alcance global (por lo que cualquiera podría decir), pero tenemos que ajustar nuestro código asíncrono en una función asíncrona para que funcione:

1
async function() {
2
    console.log(await getValues());
3
}(); // The extra "()" runs the function immediately

Incluso con envolver el código en una función, sigo afirmando que es más fácil de leer y tiene menos bytes (si elimina el comentario). Nuestro próximo ejemplo, si recuerda correctamente, hace todo en paralelo. Este es un poco complicado, porque tenemos una función interna que debe devolver una promesa. Si estamos usando la palabra clave await dentro de la función interna, esa función también debe tener el prefijo async.

1
async function foo() {
2
    try {
3
    var values = await getValues();
4
5
        var newValues = values.map(async function(value) {
6
            var newValue = await asyncOperation(value);
7
            console.log(newValue);
8
            return newValue;
9
        });
10
       
11
        return await* newValues;
12
    } catch (err) {
13
        console.log('We had an ', err);
14
    }
15
}

Es posible que haya notado el asterisco adjunto a la última palabra clave await. Esto todavía parece estar un poco en debate, pero parece que await*, básicamente, ajustará automáticamente la expresión a su derecha en Promise.all. En este momento, sin embargo, la herramienta que veremos más adelante no es compatible con await*, por lo que debe convertirse a la espera de await Promise.all(newValues); como lo estamos haciendo en el próximo ejemplo.

El siguiente ejemplo disparará las llamadas asyncOperation en paralelo, pero luego lo volverá a conectar y realizará la salida secuencialmente.

1
async function foo() {
2
    try {
3
    var values = await getValues();
4
        var newValues = await Promise.all(values.map(asyncOperation));
5
6
        newValues.forEach(function(value) {
7
            console.log(value);
8
        });
9
10
        return newValues;
11
    } catch (err) {
12
        console.log('We had an ', err);
13
    }
14
}

Me encanta eso Eso es extremadamente limpio. Si eliminamos las palabras clave await y async, quitamos el contenedor Promise.all e hicimos getValues y asyncOperation sincrónicos, este código funcionaría exactamente de la misma manera, excepto que sería sincrónico. Eso es esencialmente lo que pretendemos lograr.

Nuestro último ejemplo, por supuesto, tendrá todo funcionando secuencialmente. No se realizan operaciones asíncronas hasta que se completa la anterior.

1
async function foo() {
2
    try {
3
    var values = await getValues();
4
5
        return await values.reduce(async function(values, value) {
6
            values = await values;
7
            value = await asyncOperation(value);
8
            console.log(value);
9
            values.push(value);
10
            return values;
11
        }, []);
12
    } catch (err) {
13
        console.log('We had an ', err);
14
    }
15
}

Una vez más, estamos haciendo una función interna async. Hay un capricho interesante revelado en este código. Aprobé [] como el valor de "memo" para reduce, pero luego utilicé await en él. No se requiere que el valor del derecho de await sea una promesa. Puede tomar cualquier valor, y si no es una promesa, no lo esperará; solo se ejecutará sincrónicamente. Por supuesto, sin embargo, después de la primera ejecución de la devolución de llamada, en realidad vamos a trabajar con una promesa.

Este ejemplo es más o menos como el primer ejemplo, excepto que estamos usando reduce en lugar de map para que podamos await la operación anterior, y luego porque estamos usando reduce para construir una matriz (no algo que normalmente harías, especialmente si está construyendo una matriz del mismo tamaño que la matriz original), necesitamos construir la matriz dentro de la devolución de llamada para reduce.

Uso de las funciones de Async hoy

Ahora que has vislumbrado la simplicidad y la genialidad de las funciones asíncronas, puedes llorar como lo hice la primera vez que los vi. No estaba llorando de alegría (aunque casi lo hice); no, estaba llorando porque ES7 no estará aquí hasa que muera! Al menos así es como me sentía. Luego descubrí acerca de Traceur.

Traceur es escrito y mantenido por Google. Es un transpiler que convierte el código ES6 a ES5. Eso no ayuda! Bueno, no lo haría, excepto que también implementaron soporte para funciones asíncronas. Sigue siendo una función experimental, lo que significa que tendrá que decirle explícitamente al compilador que está usando esa característica, y que definitivamente querrá probar su código a fondo para asegurarse de que no haya ningún problema con la compilación.

Usar un compilador como Traceur significa que tendrás un código poco inflado y feo enviado al cliente, que no es lo que quieres, pero si usas mapas de origen, esto esencialmente elimina la mayoría de las desventajas relacionadas con el desarrollo. Leerá, escribirá y depurará el código limpio ES6 / 7, en lugar de tener que leer, escribir y depurar un lío complicado de código que debe solucionar las limitaciones del lenguaje.

Por supuesto, el tamaño del código seguirá siendo mayor que si hubiera escrito a mano el código ES5 (lo más probable), por lo que es posible que necesite encontrar algún tipo de equilibrio entre el código mantenible y el código de rendimiento, pero ese es un equilibrio que a menudo necesita para encontrar incluso sin usar un transpiler.

Usando Traceur

Traceur es una utilidad de línea de comandos que se puede instalar a través de NPM:

1
npm install -g traceur

En general, Traceur es bastante simple de usar, pero algunas de las opciones pueden ser confusas y requerir cierta experimentación. Puede ver una lista de las opciones para más detalles. El que realmente nos interesa es la opción --experimental.

Debe usar esta opción para habilitar las funciones experimentales, que es la forma en que obtenemos funciones asíncronas para que funcionen. Una vez que tiene un archivo JavaScript (main.js en este caso) con código ES6 y funciones async incluidas, puede compilarlo con esto:

1
traceur main.js --experimental --out compiled.js

También puede ejecutar el código omitiendo el --out compiled.js. No verá mucho a menos que el código tenga instrucciones console.log (u otras salidas de consola), pero al menos, puede verificar si hay errores. Sin embargo, es probable que desee ejecutarlo en un navegador. Si ese es el caso, hay algunos pasos más que debes seguir.

  1. Descargue el script traceur-runtime.js. Hay muchas maneras de obtenerlo, pero uno de los más fáciles es de NPM: npm install traceur-runtime. El archivo estará disponible como index.js dentro de la carpeta de ese módulo.
  2. En su archivo HTML, agregue una etiqueta de script para extraer el script de Traceur Runtime.
  3. Agregue otra etiqueta de script debajo del script Traceur Runtime para obtener compiled.js.

¡Después de esto, tu código debería estar en funcionamiento!

Automatizando la compilación de Traceur

Además de usar la herramienta de línea de comandos Traceur, también puede automatizar la compilación para que no tenga que seguir volviendo a su consola y volver a ejecutar el compilador. Grunt y Gulp, que son corredores de tareas automatizados, cada uno tiene sus propios complementos que puede usar para automatizar la compilación de Traceur: grunt-traceur y gulp-traceur, respectivamente.

Cada uno de estos corredores de tareas se puede configurar para ver su sistema de archivos y volver a compilar el código en el momento en que guarda los cambios en sus archivos de JavaScript. Para aprender a usar Grunt o Gulp, revise su documentación "Primeros pasos".

Conclusión

Las funciones asincrónicas de ES7 ofrecen a los desarrolladores una forma de salir del infierno de las retrollamadas de una manera que las promesas nunca podrían lograr por sí mismas. Esta nueva característica nos permite escribir código asíncrono de una manera muy similar a nuestro código síncrono, y aunque ES6 todavía está esperando su lanzamiento completo, ya podemos usar funciones asíncronas hoy en día a través de la transpilación. ¿Que estas esperando? ¡Sal y haz que tu código sea impresionante!