Si només esteu començant amb la programació, és probable que esteu pensant en els programes com un conjunt de blocs seqüencials de lògica, on cada bloc fa una cosa específica i passa el seu resultat perquè el següent bloc pugui executar-se i així successivament, i per al la major part tens raó, la majoria dels programes s'executen de manera seqüencial, aquest model ens permet construir programes senzills d'escriure i mantenir. Tanmateix, hi ha casos d'ús específics en què aquest model seqüencial no funcionaria o no seria òptim. Com a exemple, considereu una aplicació de lectura de llibres. Aquesta aplicació té algunes funcions avançades, com ara trobar totes les ocurrències d'una paraula, navegar entre adreces d'interès i similars. Ara imagineu que l'usuari està llegint un llibre llarg i decideix buscar totes les ocurrències d'una paraula comuna com ara "El". L'aplicació normalment trigarà un parell de segons a trobar i indexar totes les ocurrències d'aquesta paraula. En un programa seqüencial, l'usuari no pot interactuar amb l'aplicació (canviar la pàgina o ressaltar un text) fins que s'hagi completat l'operació de cerca. Amb sort, podeu veure que aquesta no és una experiència d'usuari òptima!
El diagrama il·lustra un flux d'execució típic de l'aplicació lectora de llibres. Si l'usuari inicia una operació de llarga durada (en aquest cas la cerca de totes les ocurrències de "el" en un llibre gran), l'aplicació es "congela" durant tota la durada d'aquesta operació. En aquest cas, l'usuari continuarà fent clic al botó següent de marcador sense cap resultat fins que s'acabi l'operació de cerca i totes les operacions tindran efecte alhora, donant a l'usuari final la sensació d'una aplicació endarrerida.
Potser haureu notat que aquest exemple no es correspon realment amb el model seqüencial que vam introduir anteriorment. Això es deu al fet que les operacions aquí són independents les unes de les altres. L'usuari no necessita saber el nombre d'ocurrències de "el" per anar a la següent adreça d'interès, de manera que l'ordre d'execució de les operacions no és realment important. No hem d'esperar al final de l'operació de cerca per poder navegar al següent marcador. Una possible millora del flux d'execució anterior es basa en aquesta lògica: podem executar l'operació de cerca llarga en segon pla, procedir amb qualsevol operació entrant i, un cop feta l'operació llarga, simplement podem notificar a l'usuari. El flux d'execució esdevé el següent:
Amb aquest flux d'execució, l'experiència de l'usuari es millora significativament. Ara l'usuari pot iniciar una operació de llarga durada, procedir a utilitzar l'aplicació amb normalitat i rebre una notificació un cop finalitzada l'operació. Aquesta és la base de la programació asíncrona.
Javascript, entre altres llenguatges, admet aquest estil de programació asíncrona proporcionant API extenses per aconseguir gairebé qualsevol comportament asíncron que se us pugui imaginar. Al final del dia, Javascript hauria de ser inherentment un llenguatge asíncron. Si ens referim a l'exemple anterior, la lògica asíncrona és a la base de totes les aplicacions d'interacció amb l'usuari, i Javascript es va crear principalment per utilitzar-lo al navegador on la majoria dels programes responen a les accions de l'usuari.
El següent us donarà una breu guia sobre Javascript asíncron:
Devolució de trucades
En un programa típic, normalment trobareu una sèrie de funcions. Per utilitzar una funció, l'anomenem amb un conjunt de paràmetres. El codi de funció s'executarà i retornarà un resultat, res d'extraordinari. La programació asíncrona canvia lleugerament aquesta lògica. Tornant a l'exemple de l'aplicació lectora de llibres, no podem utilitzar una funció normal per implementar la lògica de l'operació de cerca, ja que l'operació triga un temps desconegut. Una funció normal tornarà bàsicament abans que es faci l'operació, i aquest no és el comportament que esperem. La solució és especificar una altra funció que s'executarà un cop feta l'operació de cerca. Això modela el nostre cas d'ús ja que el nostre programa pot continuar el seu flux amb normalitat i un cop finalitzada l'operació de cerca, s'executarà la funció especificada per notificar a l'usuari els resultats de la cerca. Aquesta funció és el que anomenem funció de devolució de trucada:
// Search occurrences function
function searchOccurrences(word, callback) {
try {
// search operation logic, result is in result variable
//....
callback(null, word, result);
} catch (err) {
callback(err);
}
}
// Search occurrences callback function
function handleSearchOccurrencesResult(err, word, result) {
if (err) {
console.log(`Search operation for ${word} ended with an error`);
} else console.log(`Search results for ${word}: ${result}`);
return;
}
searchOccurrences("the", handleSearchOccurrencesResult);
En primer lloc, definim la funció d'operació de cerca, searchOccurrences. Es necessita la paraula per cercar i un segon paràmetre "callback" que serà la funció a executar un cop feta l'operació de cerca. La funció d'operació de cerca es va mantenir intencionadament abstracta, només hem de centrar-nos en els seus dos possibles resultats: el primer cas és on tot ha anat correctament i tenim el resultat de la cerca a la variable resultat. En aquest cas, només hem de cridar la funció de devolució de trucada amb els paràmetres següents: el primer paràmetre és nul, és a dir que no s'ha produït cap error, el segon paràmetre és la paraula que s'ha cercat i el tercer i potser més important dels tres., és el resultat de l'operació de cerca.
El segon cas és on es produeix un error, aquest també és un cas en què es fa l'execució de l'operació de cerca i hem de cridar a la funció de devolució de trucada. Utilitzem un bloc try and catch per interceptar qualsevol error i només cridem a la funció de devolució de trucada amb l'objecte d'error del bloc catch.
Aleshores vam definir la funció de devolució de trucada, handleSearchOccurrences, vam mantenir la seva lògica força simple. Només es tracta d'imprimir un missatge a la consola. Primer comprovem el paràmetre "err" per veure si s'ha produït algun error a la funció principal. En aquest cas, només informem a l'usuari que l'operació de cerca va acabar amb un error. Si no s'ha produït cap error, imprimim un missatge amb el resultat de l'operació de cerca.
Finalment, anomenem la funció searchOccurrences amb la paraula "the". La funció ara s'executarà amb normalitat sense bloquejar el programa principal i un cop feta la cerca, s'executarà la devolució de trucada i obtindrem el missatge de resultat amb el resultat de la cerca o el missatge d'error.
És important esmentar aquí que només tenim accés a la variable de resultat dins de les funcions principal i de devolució de trucada. Si provem alguna cosa com això:
let result;
function searchOccurrences(word, callback) {
try {
// search operation logic, result is in searchResult variable
//....
result = searchResult;
callback(null, word, result);
} catch (err) {
callback(err);
}
}
searchOccurrences("the", handleSearchOccurrencesResult);
console.log(result);
el resultat de la impressió estaria sense definir perquè els programes no esperen que s'executi la funció searchOccurrences. Passa a la següent instrucció, que és la instrucció d'impressió abans que la variable resultat s'assigni dins de la funció principal. Com a resultat, imprimirem la variable de resultat no assignada.
Per tant, basant-nos en aquesta lògica, hauríem de mantenir tot el codi que utilitza la variable de resultat dins de la funció de devolució de trucada. Això pot no semblar un problema ara, però podria convertir-se ràpidament en un problema real. Imagineu el cas en què tenim una cadena de funcions asíncrones que s'han d'executar en seqüència. En la lògica típica de devolució de trucada, implementareu alguna cosa com això:
functionA(function (err, resA) {
///......
functionB(resA, function (err, resB) {
///......
functionC(resB, function (err, resC) {
///......
functionD(resC, function (err, resD) {
///......
});
});
});
});
Tingueu en compte que cada devolució de trucada té un paràmetre d'error i que cada error s'ha de gestionar per separat. Això fa que el codi ja complex anterior sigui encara més complex i difícil de mantenir. Tant de bo, les promeses estiguin aquí per resoldre el problema de l'infern de devolució de trucada, ho cobrirem a continuació.
Promeses
Les promeses es creen a sobre de les devolució de trucades i funcionen de manera similar. Es van introduir com a part de les funcions d'ES6 per resoldre alguns dels problemes flagrants amb les devolució de trucades, com ara l'infern de la devolució de trucades. Les promeses proporcionen les seves pròpies funcions que s'executen en finalitzar amb èxit (resolució) i quan es produeixen errors (rebutjar). A continuació es mostra l'exemple de searchOccurrences implementat amb promeses:
// Search occurrences function
function searchOccurrences(word) {
return new Promise((resolve, reject) => {
try {
// search operation logic, result is in result variable
//....
resolve(word, result);
} catch (err) {
reject(err);
}
});
}
searchOccurrences("the")
.then((word, result) => {
console.log(`Search results for ${word}: ${result}`);
})
.catch((err) => {
console.log(`Search operation ended with an error`);
});
Repassem els canvis que hem aplicat:
La funció searchOccurrences retorna una promesa. Dins de la promesa, mantenim la mateixa lògica: tenim dues funcions per resoldre i rebutjar que representen les nostres devolució de trucada en lloc de tenir una única funció de devolució de trucada que gestioni tant una execució correcta com una execució amb errors. Les promeses separen els dos resultats i proporcionen una sintaxi neta en cridar la funció principal. La funció de resolució està "enganxada" a la funció principal mitjançant la paraula clau "then". Aquí només especifiquem els dos paràmetres de la funció de resolució i imprimim el resultat de la cerca. Una cosa semblant s'aplica a la funció de rebutjar, es pot connectar amb la paraula clau "catch". Amb sort, podreu apreciar els avantatges que ofereixen les promeses en termes de llegibilitat i neteja del codi. Si encara esteu debatint-ho, mireu com podem resoldre el problema de l'infern de devolució de trucada encadenant les funcions asíncrones per executar-les una darrere l'altra:
searchOccurrences("the")
.then(searchOccurrences("asynchronous"))
.then(searchOccurrences("javascript"))
.then(searchOccurrences("guide"))
.catch((err) => {
console.log(`Search operation ended with an error`);
});
Async/Espera
Async/Await són l'última incorporació al nostre cinturó d'eines asíncron en Javascript. Introduïts amb ES8, proporcionen una nova capa d'abstracció a la part superior de les funcions asíncrones simplement "esperant" l'execució d'una operació asíncrona. El flux del programa es bloqueja en aquesta instrucció fins que es retorna un resultat de l'operació asíncrona i després el programa procedirà amb la següent instrucció. Si esteu pensant en el flux d'execució síncrona, teniu raó. Hem acabat el cercle! Async/wait intenta portar la simplicitat de la programació síncrona al món asíncron. Tingueu en compte que això només es percep en l'execució i el codi del programa. Tot segueix igual sota el capó, Async/wait segueix fent servir promeses i les devolucions de trucades són els seus components bàsics.
Repassem el nostre exemple i implementem-lo mitjançant Async/wait:
async function searchOccurrences(word) {
try {
// search operation logic, result is in result variable
//....
return result;
} catch (err) {
console.log(`Search operation for ${word} ended with an error`);
}
}
const word = "the";
const result = await searchOccurrences(word, handleSearchOccurrencesResult);
console.log(`Search results for ${word}: ${result}`);
El nostre codi no ha canviat gaire, el més important a tenir en compte aquí és la paraula clau "async" abans de la declaració de la funció searchOccurrences. Això indica que la funció és asíncrona. A més, observeu la paraula clau "esperar" quan truqueu la funció searchOccurrences. Això indicarà al programa que espere l'execució de la funció fins que es retorni el resultat abans que el programa pugui passar a la següent instrucció, és a dir, la variable de resultat sempre mantindrà el valor retornat de la funció searchOccurrences i no la promesa de la funció, en aquest sentit, Async/Await no té un estat pendent com a Promeses. Un cop feta l'execució, passem a la instrucció d'impressió i aquesta vegada el resultat conté realment el resultat de l'operació de cerca. Com era d'esperar, el nou codi té el mateix comportament que si fos sincrònic.
Una altra cosa menor que cal tenir en compte és que, com que ja no tenim funcions de devolució de trucada, hem de gestionar l'error searchOccurrences dins de la mateixa funció, ja que no podem propagar l'error a la funció de devolució de trucada i gestionar-lo allà. Aquí només estem imprimint un missatge d'error en cas d'error per l'exemple.
Conclusió
En aquest article vam repassar els diferents enfocaments utilitzats per implementar la lògica asíncrona en Javascript. Vam començar explorant un exemple concret de per què hauríem de passar de l'estil sincrònic normal de programació al model asíncron. Després vam passar a les devolucions de trucada, que són els principals blocs de construcció de Javascript asíncron. Les limitacions de les devolució de trucades ens van portar a les diferents alternatives que es van anar afegint al llarg dels anys per superar aquestes limitacions, principalment promeses i Async/wait. La lògica asíncrona es pot trobar a qualsevol lloc del web, ja sigui que truqueu a una API externa, inicieu una consulta de base de dades, escriviu al sistema de fitxers local o fins i tot esperant l'entrada de l'usuari en un formulari d'inici de sessió. Tant de bo, ara us sentiu més segurs per abordar aquests problemes escrivint un Javascript asíncron net i de manteniment!
Si t'agrada aquest article, fes una ullada al Blog CLA on parlem de diversos temes sobre com entrar en tecnologia. A més, fes una ullada al nostre canal de youtube per veure els nostres tallers gratuïts anteriors i segueix-nos a les xarxes socials. -labs-academy/) perquè no et perdis les properes!