请注意,使函数异步不会导致它在另一个线程中自动执行,也不是“并行”执行的神奇方法。如果我们异步调用的例程要进行占用 CPU 的计算,那么 GUI 线程也会受到影响。只有当异步函数利用阻塞输入/输出(通常是网络或磁盘访问)时,它才能受益,因为这样它就不会等待输入/输出操作完成,而是立即返回,并且稍后才会返回。操作已完成(其他进程,如 op 或浏览器中的另一个线程将完成它),任务恢复,您可以获得响应。
// Versión asíncrona. Se supone que asinc1() y asinc2() son funciones que admiten
// un callback como parámetro, al cual llamarán pasándole el resultado
function main() {
asinc1(parametros, function(r1){
// Tenemos el resultado de asinc1
asinc2(r1, function(r2) {
console.log("Resultado final: " + r2);
});
});
}
// Versión con Promises
// Ahora asinc1 y asinc2 se supone que retornan una Promise
function main() {
asinc1(parametros)
.then(function(r1){ return asinc2(r1); })
.then(function(r2){
console.log("Resultado final: " + r2);
})
}
// Lo anterior puede escribirse aún más concisamente así:
function main() {
asinc1(parametros)
.then(asinc2)
.then(function(r2){
console.log("Resultado final: " + r2);
})
}
function callbackFunction(event) {
const resultado = JSON.parse(event.target.responseText);
console.log(resultado.name)
}
//función que hace una llamada ajax
function myFetch(url, callback) {
const xhr = new XMLHttpRequest();
xhr.open("GET", url)
xhr.addEventListener('load',callback);
xhr.send();
console.log('Petición hecha');
}
//ahora podemos hacer varias llamadas en una línea:
myFetch('https://swapi.dev/api/people/1/', callbackFunction);
myFetch('https://swapi.dev/api/people/2/', callbackFunction);
myFetch('https://swapi.dev/api/people/3/', callbackFunction);
//función que hace una llamada ajax
function myFetch(url, callback) {
const xhr = new XMLHttpRequest();
xhr.open("GET", url)
xhr.addEventListener('load',callback);
xhr.send();
console.log('Petición hecha');
}
//llamadas AJAX anidadas
myFetch('https://swapi.dev/api/people/1/', function callbackFunction({target}) {
const personaje = JSON.parse(target.responseText);
console.log('Personaje:',personaje.name);
console.log('Nació en',personaje.homeworld, '(oops, necesitamos otra llamada)');
//por seguridad las llamadas serán vía HTTPS
const url = personaje.homeworld.replace('http','https');
myFetch(url, function callbackfunction2({target}) {
const planeta = JSON.parse(target.responseText);
console.log('El planeta era:',planeta.name);
const url2 = planeta.films[0].replace('http','https');
myFetch(url2, function callbackfunction3({target}) {
const film = JSON.parse(target.responseText);
console.log('El título de la primera película en la que aparece es', film.title);
}); //fin tercer callback
}); //fin segundo callback
}); //fin primer callback
在这种情况下,我们仍然有一些可管理的东西(3 个嵌套调用),但是这种嵌套会导致我们所谓的回调地狱:
承诺
如何避免?嗯,有承诺。Promise 是一个对象,它允许您链接异步操作,以更简单的方式说“执行操作 A,当您完成 A 时,执行 B,然后执行 C,然后执行……” :
//Creemos promesas!!
function myFetchPrometido(url) {
//creamos una promesa que se resuelve cuando la llamada AJAX obtiene una respuesta
return new Promise(function (tenemosRespuesta, tenemosError) {
const xhr = new XMLHttpRequest();
xhr.open("GET", url)
xhr.addEventListener('load',({target}) => {
tenemosRespuesta(JSON.parse(target.responseText));
});
xhr.send();
console.log('Petición hecha');
});
}
//ahora podemos hacer varias llamadas dependientes
// sin anidamiento extra.
const promesa = myFetchPrometido('https://swapi.dev/api/people/1/');
promesa.then(personaje => {
console.log('Personaje:',personaje.name);
console.log('Nació en',personaje.homeworld, '(oops, necesitamos otra llamada)');
//por seguridad las llamadas serán vía HTTPS
return myFetchPrometido(personaje.homeworld.replace('http','https'));
}).then(planeta => {
console.log('Planeta', planeta.name);
return myFetchPrometido(planeta.films[0].replace('http','https'));
}).then(film => {
console.log('Primera aparición en la película', film.title);
});
Como vemos, una promesa tiene el método then, al que se le pasa la función callback. Lo bueno de este método es que si la función callback devuelve algo, el método lo devuelve a su vez como el resultado de una nueva promesa (si la función devuelve una promesa, no se anidan, es así de inteligente), con lo que puedes tener un número indefinido de métodos then encadenados.
async / await
De este modo ya nos hemos ahorrado una anidación exagerada, pero el código sigue siendo un poco feo. ¿Se puede hacer más elegante?
La respuesta es sí: existe una sintaxis que es equivalente a usar Promesas pero las "oculta", haciendo que el código parezca síncrono, sin necesidad de callbacks, anidadas o no:
//Creemos promesas!!
function myFetchPrometido(url) {
//creamos una promesa que se resuelve cuando la llamada AJAX obtiene una respuesta
return new Promise(function (tenemosRespuesta, tenemosError) {
const xhr = new XMLHttpRequest();
xhr.open("GET", url)
xhr.addEventListener('load',({target}) => {
tenemosRespuesta(JSON.parse(target.responseText));
});
xhr.send();
console.log('Petición hecha');
});
}
// await sólo se puede usar dentro de una función asíncrona,
// es la única limitación o pega
async function main () {
//await es literalmente "espera", estamos esperando la resolución de una promesa
const personaje = await myFetchPrometido('https://swapi.dev/api/people/1/');
console.log('Personaje:',personaje.name);
console.log('Nació en',personaje.homeworld, '(oops, necesitamos otra llamada)');
const planeta = await myFetchPrometido(personaje.homeworld.replace('http','https'));
console.log('Planeta', planeta.name);
const film = await myFetchPrometido(planeta.films[0].replace('http','https'));
console.log('Primera aparición en la película', film.title);
};
//llamamos a la función asíncrona
main();
让我们假设以下假设情况,我们向 API 发出请求(此 API 返回一个产品数组),默认情况下,它会有延迟或“延迟”。
let productos = [1,2,3];
const conseguirProductos= ()=>{
return new Promise((resolve,reject)=>{//creamos nuestra promesa
console.log('Cargando productos... esto puede dilatar un poco');//le damos un mensaje al usuario
setTimeout(()=>{
resolve(productos)
},5010);
});
}
async function getMisProd()//para indicar que es una funcion asincrona debe colocar la palabra reservada 'async'
{
let misP= await conseguirProductos();//podriamos decir que 'await' remplaza el 'then'
console.log('Mira, este es el resultado '+misP);
}//esto lo definimos como 'azuzar sintactia' /
//++asyncs++ a +++await+++ lo usamos en algo que tenga ver con una promesa o algo que tenga cierta latencia...
getMisProd();//llamamos a la función
//esto seria usando la forma 'normal' sin el await
let productos = [1,2,3];
const conseguirProductos= ()=>{
return new Promise((resolve,reject)=>{//cremos nuestra promesa
console.log('Cargando productos...esto puede dilatar un poco');//le damos un mensaje al usuario
setTimeout(()=>{
resolve(productos)
},5010);
});
}
conseguirProductos()
.then(datos=>{//en caso de que sea exitosa se ejecuta 'then'
console.log(datos);
});
想象一下,您必须调用一个需要时间来响应的函数。但是您需要它的答案才能稍后调用另一个,因为第二个需要第一个产生的参数作为参数。例如,第一个可能会下载一个网页,从中提取一个 URL,而第二个可能需要这个 URL 来访问另一个资源。
使用同步代码,一切都简单易懂
因为我们假设对
serv1()
和的调用serv2()
是同步进行的,也就是说,在这些函数没有响应之前,执行不会继续到下一行。这种类型的代码易于编写和理解,但效率非常低。假定所花费的时间serv1()
和serv2()
获得响应主要是等待时间(例如网络通信),线程停止等待响应的时间,以及您可以用来做其他事情的时间。事实上,在 JavaScript 中,这个线程是处理与 DOM 和用户交互的线程,所以这次页面将保持“挂起”状态。如果时间太长,浏览器最终会检测到线程没有处理 GUI 事件,并会输出一个警告,如“此页面上的脚本花费的时间太长。你想中止它吗?”解决方案是利用这些函数的异步版本。让我们打电话给
asinc1()
他们asinc2()
这些函数立即返回到主线程,但让浏览器“负责”在另一个线程(专用于网络通信)中发出相应的请求。问题是这些请求完成后会发生什么?答案去哪儿了?由于我们
asinc1()
在调用之前需要响应,asinc2()
我们的程序如何知道它何时完成asinc1()
以及结果是什么?有两种可能的实现来解决这个问题:
第一个解决方案(回调)更直接,但会导致更多嵌套代码,如下例所示:
嵌套会更糟,因为我们有更多的链接函数(需要一个的结果来调用下一个),从而产生作为其他函数参数的函数,也就是其他函数的参数等。并且源代码获得了一种特征形式,其中缩进越来越多,然后在大括号和圆括号闭合时撤消该缩进,这种结构称为末日金字塔或回调地狱。
另一方面,在这段代码中,所有逻辑在某种程度上都是“倒退”的,因为函数不返回结果,而是将这些结果作为参数传递(给其他函数),而处理响应的函数依次作为参数传递。错误流(我不会在这个答案中讨论)也很复杂,因为不能再使用异常,相反通常会有两个回调函数,一个在成功时调用,一个在失败时调用。
Promises 避免了这种嵌套,并使错误处理更简单。Promise 有一个方法
then()
,您可以将函数传递给该方法,该方法将在 Promise 解决时自动执行(或者如果在我们调用时它已经解决,则立即执行.then()
)。该函数将接收作为参数的承诺值(预期结果)。最好的事情是它.then()
返回一个新的 Promise,当我们与之关联的函数被执行时,它将被解析。通过这种方式,可以将几个链接在一起.then()
以“模拟”顺序代码,例如“当这个 Promise 解决时,执行此操作,然后,当此 Promise 解决时,执行此操作,然后......)这将是新的语法:
从 2017 年开始,JavaScript 也有了单词
async/await
,它们是使用 Promises 的语法糖,新语法隐藏了它们,并使它们看起来像同步代码。如果我们
await
在函数调用前面使用这个词,则该函数被理解为返回一个 Promise。然后不知何故(我不会详细说明它是如何“内部”执行的)执行在该点暂停(但不会阻塞主线程),并且仅在 promise 已解决时才恢复,然后await
返回结果是承诺的价值。多亏了这一点,代码现在是:
将后者与前者(同步)进行比较。该结构是相同的,并且只放在
await
异步调用async
之前和函数之前(因为只有用声明的函数async
才能使用await
)。三种异步解决方案的运行时效率相似。变化很大的是代码的结构,它使用 Promises 进行了简化,使用 Promises 更是如此
await
,可能会导致更少的错误,以及更好的可维护性和调试性。要回答您的问题,您必须先回答一些基本问题
什么是异步?
该术语在 Javascript 中使用松散,程序员通常将其与在不阻塞 Javascript 线程的情况下执行任务的能力相关联,这是完全错误的。异步实际上意味着在事件循环或事件循环的不同阶段执行指令的能力,正如英语中所说的那样。例如,您可以完美地安排作业,并且在运行时会阻塞主线程。
setTimeout
除非您准备好看到浏览器崩溃并且我正在使用 a来安排包含循环
setTimeout
的任务,否则不要按此代码段上的按钮。tareaIntensiva
在任何情况下,浏览器都会向您显示一个迹象,表明有一个脚本阻止了该页面。事件循环由不同的消息或函数以非常简化的方式组成,这些消息或函数使用特定的优先级系统按照预先确定的顺序一个接一个地执行,直到没有更多消息要处理。在另一个更高级的抽象模型中,它由普通用户代码、微任务和宏任务组成。这个主题非常复杂,但您可以在此处找到有关一切如何运作的相当广泛而详细的文章。使用时以简化的方式解释它
setTimeout
您创建了一个宏任务,该任务将在稍后的某个周期中执行,promises 创建微任务将在执行您编写的所有传统代码之后立即在您所在的同一周期中执行,而无需这些工件。因此,如果您调用setTimeout
和承诺,后者将首先执行。我必须解释以上所有内容,以便您了解它如何应用于您的问题:
回调
就样式而言,它们是最糟糕的决定,因为众所周知,它们会创建使嵌套代码完全不可读的回调地狱。关于 Nodejs 中的错误处理,有一个将错误作为第一个参数传递的约定,但这个标准可能会被违反,因为回调是作为参数传递的简单函数。
在执行方面,回调是不可知的,因为它依赖于实现者使用微任务或宏任务来执行它们。这可能根本不会引起异步。这是一个例子:
此无害代码仅在当且仅当 时在事件循环的后期执行
param > 20
,否则与编写示例相同:这段代码甚至没有运行一个有时会让人头疼的任务,试图找出问题所在。在这种情况下,正确的做法是使用它
setTimeout
来模拟异步,并且无论参数如何,函数的行为方式都相同。承诺
它们通常使用微任务执行(只有在没有其他可用的极端情况下才使用宏任务)。如果您在 Node 和浏览器中都使用本机 Promise,则可以保证它们是微任务,因此您可以放心地说,无论它们做什么,Promise都是异步的。
从风格上讲,它是对回调的改进,但实际上两者都
then
使用catch
回调来安排代码执行。优点是不会创建金字塔,因为可以链接承诺。除其他原因外,它们旨在使异步代码保留同步代码的属性,例如平面缩进和单个错误通道,这是纯回调无法实现的。重要的是要了解,您可以根据需要创建一个链,因为每次您返回
then
一个新的承诺时都会创建一个新的承诺(undefined
如果您不返回任何内容,则返回一个函数),但对于每个人来说,then
一个新的微-task 被编程为虽然它比宏任务便宜,但它在事件循环中的成本仍然很小。许多开发人员忘记的一个方面是,您应该始终在链的末尾处理错误,因为例如,在 Node 中,未捕获的错误承诺可能会终止进程。记住这一点很重要,因为我将在下一点中说。
异步/等待
它是 promise 的语法糖。出于同样的原因,他们也使用微任务。有人声称它们可以优化得更快。这当然取决于浏览器,并不是每次都 100% 安全。
至于样式“承诺”,缩进比承诺少,但如果你记得我之前说过的关于处理错误的内容,你将不得不编写一个
try/catch
来处理错误,并且不可避免地会创建缩进。如果您使用一些抽象模型,您可以在其他地方处理错误,变得完全平坦且非常易于阅读代码。使用 async/await 处理错误也有好处,因为函数中抛出的任何错误
async
都会被函数捕获并自动变成被拒绝的 promise。也就是说,很明显,您无法比较 Promise 和回调,因为它们实现了不同的功能,但就风格而言,如果您有真正的异步代码(例如 ajax 调用、读取文件、websocket 连接等),它们将是一个很好的选择提高可读性方面的问题。如果您的代码真的很复杂,那么 async/await将是编写和理解代码的最佳选择。
为了完整起见,您应该知道有很多方法可以创建异步,而不仅仅是 promise。您可以找到标准方法,如
setTimeout
、setInterval
、requestAnimationFrame
、process.nextTick
,setInmediate
以及非正式方法(如MutationObserver ) ,它允许您在浏览器中人为地创建微任务。如果你想做真正密集的操作,可以阻塞主线程,你应该使用Web Workers来自@devconcept 和@abulafia 的前两个答案非常详细地解释了这些概念,但我想提供一个版本以供快速参考,您可以在其中看到我们如何从回调到承诺,以及如何使用它们句法
async / await
因此,这个答案将是一个实际的例子,我们将进行异步调用并逐步重构它,直到我们获得最优雅的代码。
回调
假设我们通过 XHR 进行了经典的 AJAX 调用:
AJAX 调用是异步的,这意味着我们知道何时发出请求(
xhr.send();
),但不知道何时返回响应。我们所知道的是,当浏览器收到响应时,会调用该函数,callbackFunction
因为我们已经这样定义了它。但是现在让我们想象一下,我们想要创建一个函数,允许我们以一种简单的方式进行 AJAX 调用,在一行中进行调用:
这是一个进步,但如果我们有依赖关系,我们就会遇到问题:如果第二个调用依赖于第一个调用会发生什么?我们会留下这样的东西:
在这种情况下,我们仍然有一些可管理的东西(3 个嵌套调用),但是这种嵌套会导致我们所谓的回调地狱:
承诺
如何避免?嗯,有承诺。Promise 是一个对象,它允许您链接异步操作,以更简单的方式说“执行操作 A,当您完成 A 时,执行 B,然后执行 C,然后执行……” :
Como vemos, una promesa tiene el método
then
, al que se le pasa la función callback. Lo bueno de este método es que si la función callback devuelve algo, el método lo devuelve a su vez como el resultado de una nueva promesa (si la función devuelve una promesa, no se anidan, es así de inteligente), con lo que puedes tener un número indefinido de métodosthen
encadenados.async / await
De este modo ya nos hemos ahorrado una anidación exagerada, pero el código sigue siendo un poco feo. ¿Se puede hacer más elegante?
La respuesta es sí: existe una sintaxis que es equivalente a usar Promesas pero las "oculta", haciendo que el código parezca síncrono, sin necesidad de callbacks, anidadas o no:
Ejemplo de que es async y await:
让我们假设以下假设情况,我们向 API 发出请求(此 API 返回一个产品数组),默认情况下,它会有延迟或“延迟”。
在上面提到的情况下,我们使用了 async 和 await... 现在我将向您展示没有 async 和 await 的情况。
总结:Async 和 Await 为我们提供了更漂亮、更易于使用的语法......