解锁JavaScript异步奥秘:Promise与async/await的魔法之旅
什么是异步
异步是一种编程模型,与同步相对。在异步编程中,程序不会等待某个操作(如I/O操作、网络请求等)完成后再继续执行下一行代码,而是继续执行后续任务,无需阻塞或等待。这种方式提高了程序的效率和响应性,特别是在执行耗时操作时,能够确保用户界面或其他任务流畅进行。
下面我将以一段代码来为大家展示什么是异步:
function foo() {
setTimeout(() => {
console.log("hello")
}, 1000)
}
function bar() {
console.log("world")
}
foo();
bar();
输出结果;
为什么这段代码是先输出world
再输出hello
?
- js是单线程执行的,当遇到需要耗时的异步代码时会先将该代码挂起,再去执行后续不耗时的同步代码
- 上述代码为例,当执行到
foo
时V8引擎判断该函数为需要耗时的异步代码先将该代码挂起 - 当执行到
bar
时,V8引擎判断该函数为不耗时的同步代码将该函数执行,当该函数执行完毕后再去执行foo
函数,所以会先输出world
再输出hello
。
在早期编写异步函数的过程中会出现多层嵌套的回调函数,这种结构非常的难理解和阅读,随着嵌套层数的增加维护会变得很难,这也就是所谓的回调地狱
。如以下代码:
function getUser(id, callback) {
setTimeout(() => {
console.log(`Getting user with id ${id}`);
callback({ id: id, name: 'John Doe' });
}, 1000);
}
function getOrders(userId, callback) {
setTimeout(() => {
console.log(`Getting orders for user with id ${userId}`);
callback([{ orderId: 1, product: 'Book' }, { orderId: 2, product: 'CD' }]);
}, 1500);
}
function getOrderDetails(orderId, callback) {
setTimeout(() => {
console.log(`Getting details for order with id ${orderId}`);
callback({ orderId: orderId, details: 'Shipping to: 123 Street' });
}, 2000);
}
getUser(1, (user) => {
getOrders(user.id, (orders) => {
const firstOrderId = orders[0].orderId;
getOrderDetails(firstOrderId, (orderDetails) => {
console.log(`User ${user.name}'s first order details:`, orderDetails);
});
});
});
在这个例子中,我们首先获取一个用户,然后根据用户ID获取其订单,最后获取第一个订单的详情。每个步骤都是异步的,并且依赖于前一个步骤的结果,导致了多层嵌套的回调函数。这种结构不仅难以阅读和理解,而且随着逻辑复杂性的增加,维护和调试也会变得非常困难,这就是所谓的回调地狱
。
为了解决多层回调函数的窘境,在es6推出了一种全新的编写异步代码的方式---Promise
。
Promise
Promise(字面意思承诺
),白话理解就是给调用者一个承诺,一段时间后就会将数据返回给你,就可以创建一个promise对象。
- 当new了一个Promise时,此时需要传递一个回调函数,这个函数为立即执行的,叫做
(executor)
- 在这个回调函数中,需要传入两个参数,
reslove
,reject
- 执行
reslove
函数时,回调Promise
对象的.then
函数 - 执行了
reject
函数时,回调Promise
对象的.catch
函数
1. executor执行器
Promise的构造函数是用来实例化一个新的Promise对象的,它接受一个函数作为参数,这个函数我们称为执行器(executor)
。执行器函数在Promise创建时立即执行
。如以下代码;
let foo = new Promise((resolve, reject) => {
console.log('hello world'); // hello world
})
从这段代码可以看出传入的执行器函数executor
是会在创建Promise
对象时立即执行的。
2.Promise的状态
Promise有三种状态,这些状态描述了Promise从创建到最终完成(或失败)的过程。
- 首先给大家举一个例子更好的理解这三种状态。
- 你和好兄弟约好了周末一起开黑,但是现在还没有到周末,此时的状态就为
等待中
- 转眼间到了周末,你和好兄弟成功开黑,此时的状态为
成功
- 到了周末,你的女朋友临时要你陪她去逛街,无奈之下你只能放好兄弟的鸽子不和他们开黑了,此时的状为
拒绝
。
- 你和好兄弟约好了周末一起开黑,但是现在还没有到周末,此时的状态就为
- Pending(等待中) :这是Promise初始状态。当一个Promise刚被创建执行了
executor
但其内部的异步操作还未完成(既没有被resolve
解决,也没有被reject
拒绝)时,Promise处于等待中状态。 - Fulfilled(成功) :当Promise中的操作成功完成,通过调用
resolve
函数将Promise的状态置为成功状态
。一旦Promise变为fulfilled状态,它就不能再变为其他状态,同时会传递一个可选的成功值给到接下来的.then
方法中的回调函数。 - Rejected(拒绝) :如果Promise中的操作失败或遇到错误,通过调用
reject
函数将Promise的状态置为拒绝状态
。同样地,一旦Promise变为rejected状态,它就会保持这个状态不变,并且会传递一个可选的错误原因给到.catch
方法中的回调函数处理。
Promise的状态转换规则:
Promise
只能从Pending
转换到Fulfilled
或Rejected
,不能逆向转换,也不能多次转换。- 一旦
Promise
变为Fulfilled
或Rejected
状态,其内部的值(成功的结果或失败的原因)就被固定了,之后的任何操作都不会改变这个值,即.then
或.catch
中的回调函数在Promise
状态改变后任何时候调用都会得到同样的结果。
优点: 这些状态机的设计使得Promise能够以一种确定性的方式处理异步操作的结果,简化了异步编程的复杂度。
.then接收参数
then
方法可以接受两种不同类型参数,一种参数为成功的回调,另一种参数为失败的回调,如下实例:
const promise = new Promise((resolve, reject) => {
resolve('success')
// reject('error')
})
promise.then(res => console.log(res), rej => console.log(rej)) //success
const promise = new Promise((resolve, reject) => {
// resolve('success')
reject('error')
})
promise.then(res => console.log(res), rej => console.log(rej)) //error
.catch方法
catch方法有返回值,它的返回值是Promise
。
返回普通对象
let promise = new Promise((resolve, reject) => {
reject('error')
})
promise.catch(err => ({ text: 'hello' })).then(res => console.log(res)) // { text: 'hello' }
返回Promise
let promise = new Promise((resolve, reject) => {
reject('error')
})
promise.catch(err => {
return new Promise((resolve, reject) => {
reject('第二个错误promise')
})
}).catch(res => console.log(res))
// 第二个错误promise
在这段代码中第一次catch
返回了一个Promise
对象,该对象reject
的内容又会被catch
捕捉到。
3.Promise解决回调地狱问题
为了使用Promise解决上述代码中的回调地狱问题,我们可以将每个异步操作封装成返回Promise的函数,然后通过.then
链式调用来组织逻辑。下面是转换后的代码示例:
function getUser(id) {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log(`Getting user with id ${id}`);
resolve({ id: id, name: 'John Doe' });
}, 1000);
});
}
function getOrders(userId) {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log(`Getting orders for user with id ${userId}`);
resolve([{ orderId: 1, product: 'Book' }, { orderId: 2, product: 'CD' }]);
}, 1500);
});
}
function getOrderDetails(orderId) {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log(`Getting details for order with id ${orderId}`);
resolve({ orderId: orderId, details: 'Shipping to: 123 Street' });
}, 2000);
});
}
getUser(1)
.then(user => getOrders(user.id))
.then(orders => getOrderDetails(orders[0].orderId))
.then(orderDetails => {
console.log(`User ${user.name}'s first order details:`, orderDetails);
})
.catch(error => {
console.error('Error caught:', error);
});
在这个版本中,每个原本接收回调函数的异步操作现在都返回一个Promise。通过链式调用.then
,我们可以依次执行这些操作,而不需要嵌套的回调函数,使得代码更加扁平化和易于阅读。此外,还添加了一个.catch
来统一处理可能发生的错误。
async/await
虽然在es6中推出了Promise的用法,但是在某些大量函数嵌套的场景下使用Promise仍然不够优雅,于是在es7提出了async/await函数,这次终于让JS的异步操作变得无比的优雅。
什么是async/await
async/await
是 JavaScript 中用于处理异步操作的两个关键字,它们简化了基于Promise的异步编程模型,使得异步代码更加易于理解和维护。
- async: 这个关键字用于声明一个异步函数。声明为
async
的函数会隐式地返回一个 Promise 对象(async是Promise的语法糖)
。在异步函数内部,可以使用await
关键字。 - await: 该关键字只能在
async
函数中使用。它用于等待一个 Promise 完成(解析或拒绝)。await
使得你可以以同步的方式编写异步代码,即代码会暂停在await
处,直到等待的 Promise 完成,然后继续执行并返回Promise的结果。
状态为fulfilled
let data = null;
function getData() {
return new Promise((resolve, reject) => {
setTimeout(() => {
data = [1, 2, 3]
console.log(data)
resolve()
}, 1000)
})
}
function another() {
return new Promise((resolve, reject) => {
setTimeout(() => {
data.push(4);
resolve()
}, 1000)
})
}
function another2() {
console.log(data)
}
// async 写了 默认会返回 new Promise
async function foo() {
await getData();
await another();
another2();
}
foo();
运行结果为:
在上述代码的例子中我们可以看到,await后面的代码,其实相当于.then
的回调,当状态变为fulfilled
时才会继续执行后面的代码。
状态为rejected
function requestData(url) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (url.includes('hello')) {
resolve(url)
} else {
reject('请求错误')
}
}, 1000);
})
}
async function getData() {
try {
const res = await requestData('world')
console.log(res)
} catch (error) {
console.error(error); // 在这里处理错误
}
}
getData()
// 请求错误
通过添加 try...catch
块,当 requestData
函数中的 Promise 被拒绝时,错误会被捕获并在 catch
块中处理,这样就不会导致程序中断并能够优雅地处理错误情况。
总结
本篇文章就到此为止啦,希望通过这篇文章能对你理解JS异步编程
有所帮助,本人水平有限难免会有纰漏,欢迎大家指正。如觉得这篇文章对你有帮助的话,欢迎点赞收藏加关注,感谢支持🌹🌹。
转载自:https://juejin.cn/post/7380994802745229349