Se só estás comezando coa programación, é probable que esteas pensando nos programas como un conxunto de bloques secuenciais de lóxica, onde cada bloque fai unha cousa específica e pasa o seu resultado para que o seguinte bloque poida executarse, etc. a maior parte ten razón, a maioría dos programas execútanse de forma secuencial, este modelo permítenos construír programas que sexan sinxelos de escribir e manter. Non obstante, hai casos de uso específicos nos que este modelo secuencial non funcionaría ou non sería óptimo. Como exemplo, considere unha aplicación de lectura de libros. Esta aplicación ten algunhas funcións avanzadas, como atopar todas as ocorrencias dunha palabra, navegar entre os marcadores e similares. Agora imaxina que o usuario está lendo un libro longo e decide buscar todas as ocorrencias dunha palabra común como "O". A aplicación normalmente tardará un par de segundos en atopar e indexar todas as ocorrencias desa palabra. Nun programa secuencial, o usuario non pode interactuar coa aplicación (cambiar a páxina ou resaltar un texto) ata que se cumpra a operación de busca. Con sorte, podes ver que esa non é unha experiencia de usuario óptima.
O diagrama ilustra un fluxo de execución típico da aplicación lectora de libros. Se o usuario inicia unha operación de longa duración (neste caso a busca de todas as ocorrencias de "o" nun libro grande), a aplicación "conxelarase" durante toda a duración desa operación. Neste caso, o usuario seguirá facendo clic no seguinte botón de marcador sen resultado ata que remate a operación de busca e todas as operacións entrarán en vigor ao mesmo tempo dándolle ao usuario final a sensación dunha aplicación atrasada.
Quizais teña notado que este exemplo non se corresponde realmente co modelo secuencial que introducimos anteriormente. Isto débese a que as operacións aquí son independentes entre si. O usuario non precisa saber sobre o número de ocorrencias de "o" para navegar ata o seguinte marcador, polo que a orde de execución das operacións non é realmente importante. Non temos que esperar ao final da operación de busca para poder navegar ata o seguinte marcador. Unha posible mellora do fluxo de execución anterior baséase nesta lóxica: podemos executar a operación de busca longa en segundo plano, proceder con calquera operación entrante e, unha vez realizada a operación longa, simplemente podemos avisar ao usuario. O fluxo de execución pasa a ser o seguinte:
Con este fluxo de execución, a experiencia do usuario mellora significativamente. Agora o usuario pode iniciar unha operación de longa duración, proceder a usar a aplicación normalmente e recibir unha notificación unha vez que se faga a operación. Esta é a base da programación asíncrona.
Javascript, entre outras linguaxes, admite este estilo de programación asíncrona proporcionando amplas API para lograr case calquera comportamento asíncrono que se poida pensar. Ao final do día, Javascript debería ser inherentemente unha linguaxe asíncrona. Se nos referimos ao exemplo anterior, a lóxica asíncrona está na base de todas as aplicacións de interacción do usuario, e Javascript foi construído principalmente para ser usado no navegador onde a maioría dos programas tratan de responder ás accións do usuario.
O seguinte darlle unha breve guía sobre Javascript asíncrono:
Devolucións de chamada
Nun programa típico, normalmente atoparás unha serie de funcións. Para usar unha función, chamámola cun conxunto de parámetros. O código da función executarase e devolverá un resultado, nada fóra do común. A programación asíncrona cambia lixeiramente esta lóxica. Volvendo ao exemplo da aplicación lectora de libros, non podemos utilizar unha función normal para implementar a lóxica de operación de busca xa que a operación leva un tempo descoñecido. Unha función normal volverá basicamente antes de que se faga a operación, e este non é o comportamento que esperamos. A solución é especificar outra función que se executará unha vez realizada a operación de busca. Isto modela o noso caso de uso xa que o noso programa pode continuar o seu fluxo con normalidade e unha vez que remate a operación de busca, executarase a función especificada para notificar ao usuario os resultados da busca. Esta función é o que chamamos función de devolución de chamada:
// 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 primeiro lugar, definimos a función de operación de busca, searchOccurrences. Leva a palabra para buscar e un segundo parámetro "callback" que será a función a executar unha vez que se faga a busca. A función de operación de busca mantívose intencionadamente abstracta, só necesitamos centrarnos nos seus dous posibles resultados: o primeiro caso é onde todo foi exitoso e temos o resultado da busca na variable resultado. Neste caso, só temos que chamar á función de devolución de chamada cos seguintes parámetros: o primeiro parámetro é nulo, o que significa que non se produciu ningún erro, o segundo parámetro é a palabra que se buscou e o terceiro e quizais o máis importante dos tres., é o resultado da operación de busca.
O segundo caso é onde se produce un erro, este tamén é un caso no que se realiza a execución da operación de busca e temos que chamar á función de devolución de chamada. Usamos un bloque try and catch para interceptar calquera erro e só chamamos á función de devolución de chamada co obxecto de erro do bloque catch.
Despois definimos a función de devolución de chamada, handleSearchOccurrences, mantivemos a súa lóxica bastante sinxela. É só cuestión de imprimir unha mensaxe na consola. Primeiro comprobamos o parámetro "err" para ver se se produciu algún erro na función principal. Nese caso, só avisamos ao usuario de que a operación de busca rematou cun erro. Se non aparece ningún erro, imprimimos unha mensaxe co resultado da operación de busca.
Finalmente, chamamos á función searchOccurrences coa palabra "o". A función agora executarase con normalidade sen bloquear o programa principal e unha vez realizada a busca, executarase a devolución de chamada e obteremos a mensaxe de resultado co resultado da busca ou a mensaxe de erro.
É importante mencionar aquí que só temos acceso á variable de resultado dentro das funcións main e callback. Se probamos algo así:
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);
o resultado da impresión estaría indefinido porque os programas non esperan a que se execute a función searchOccurrences. Pasa á seguinte instrución que é a instrución de impresión antes de que a variable resultado sexa asignada dentro da función principal. Como resultado, imprimiremos a variable de resultado sen asignar.
Polo tanto, con base nesta lóxica, debemos manter todo o código que usa a variable resultado dentro da función de devolución de chamada. Quizais non pareza un problema agora, pero podería converterse rapidamente nun problema real. Imaxina o caso no que temos unha cadea de funcións asíncronas que deben executarse en secuencia. Na lóxica típica de devolución de chamada, implementarías algo así:
functionA(function (err, resA) {
///......
functionB(resA, function (err, resB) {
///......
functionC(resB, function (err, resC) {
///......
functionD(resC, function (err, resD) {
///......
});
});
});
});
Teña en conta que cada devolución de chamada ten un parámetro de erro e que cada erro ten que tratarse por separado. Isto fai que o código xa complexo anterior sexa aínda máis complexo e difícil de manter. Con sorte, Promises estean aquí para resolver o problema do inferno de devolución de chamadas, cubrirémolo a continuación.
Promesas
As promesas constrúense enriba das devolucións de chamada e funcionan dun xeito similar. Introducíronse como parte das funcións de ES6 para resolver algúns dos problemas evidentes coas devolucións de chamada, como o inferno de devolución de chamada. As promesas proporcionan as súas propias funcións que se executan ao finalizar correctamente (resolver) e cando se producen erros (rexeitar). O seguinte mostra o exemplo searchOccurrences implementado con promesas:
// 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`);
});
Imos repasar os cambios que aplicamos:
A función searchOccurrences devolve unha promesa. Dentro da promesa, mantemos a mesma lóxica: temos dúas funcións de resolución e rexeitamento que representan as nosas devolucións de chamada en lugar de ter unha única función de devolución de chamada que xestiona tanto unha execución exitosa como unha execución con erros. As promesas separan os dous resultados e proporcionan unha sintaxe limpa ao chamar á función principal. A función de resolución está "enganchada" á función principal mediante a palabra clave "entonces". Aquí só especificamos os dous parámetros da función de resolución e imprimimos o resultado da busca. Unha cousa semellante aplícase á función de rexeitamento, pódese enganchar usando a palabra clave "catch". Con sorte, pode apreciar as vantaxes que ofrecen as promesas en termos de lexibilidade e limpeza do código. Se aínda estás a debatelo, consulta como podemos resolver o problema do inferno de devolución de chamadas encadeando as funcións asíncronas para executalas unha tras outra:
searchOccurrences("the")
.then(searchOccurrences("asynchronous"))
.then(searchOccurrences("javascript"))
.then(searchOccurrences("guide"))
.catch((err) => {
console.log(`Search operation ended with an error`);
});
Asíncrono/Agardar
Async/Await son a última incorporación ao noso cinto de ferramentas asíncronos en Javascript. Introducidos con ES8, proporcionan unha nova capa de abstracción enriba das funcións asíncronas simplemente "esperando" a execución dunha operación asíncrona. O fluxo do programa bloquea nesa instrución ata que se devolve un resultado da operación asíncrona e, a continuación, o programa procederá coa seguinte instrución. Se estás a pensar no fluxo de execución síncrona tes razón. Chegamos o círculo! Async/wait tenta achegar a sinxeleza da programación síncrona ao mundo asíncrono. Teña en conta que só se percibe na execución e no código do programa. Todo segue igual baixo o capó, Async/wait seguen usando promesas e as devolucións de chamada son os seus bloques de construción.
Repasemos o noso exemplo e implementémolo usando 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}`);
O noso código non cambiou moito, o importante a ter en conta aquí é a palabra clave "async" antes da declaración da función searchOccurrences. Isto indica que a función é asíncrona. Ademais, teña en conta a palabra clave "esperar" ao chamar á función searchOccurrences. Isto indicará ao programa que agarde a execución da función ata que se devolva o resultado antes de que o programa poida pasar á seguinte instrución, é dicir, a variable de resultado sempre manterá o valor devolto da función searchOccurrences e non a promesa de a función, nese sentido, Async/Await non ten un estado pendente como Promises. Unha vez rematada a execución, pasamos á instrución print e esta vez o resultado contén realmente o resultado da operación de busca. Como era de esperar, o novo código ten o mesmo comportamento que se fose sincrónico.
Outra cousa menor a ter en conta é que, dado que xa non temos funcións de devolución de chamada, necesitamos xestionar o erro searchOccurrences dentro da mesma función xa que non podemos só propagar o erro á función de devolución de chamada e manexalo alí. Aquí só estamos imprimindo unha mensaxe de erro en caso de erro por mor do exemplo.
Conclusión
Neste artigo repasamos os diferentes enfoques utilizados para implementar a lóxica asíncrona en Javascript. Comezamos explorando un exemplo concreto de por que teriamos que pasar do estilo de programación sincrónico normal ao modelo asíncrono. Despois pasamos ás devolucións de chamada, que son os principais bloques de construción de Javascript asíncrono. As limitacións das devolucións de chamada leváronnos ás diferentes alternativas que se foron engadindo ao longo dos anos para superar estas limitacións, principalmente promesas e Async/wait. A lóxica asíncrona pódese atopar en calquera lugar da web, tanto se estás chamando a unha API externa, iniciando unha consulta de base de datos, escribindo no sistema de ficheiros local ou mesmo esperando a entrada do usuario nun formulario de inicio de sesión. Con sorte, agora te sintas máis seguro para abordar estes problemas escribindo Javascript asíncrono limpo e mantible.
Se che gusta este artigo, consulta o Blog CLA onde falamos de varios temas sobre como entrar na tecnoloxía. Ademais, consulta a nosa canle de YouTube para coñecer os nosos obradoiros gratuítos anteriores e síguenos nas redes sociais. -labs-academy/) para que non te perdas os próximos!
Proporciona a túa carreira para o futuro mellorando as habilidades en HTML, CSS e JavaScript co Code Labs Academy campo de arranque de desenvolvemento web.