1. 异步编程基础
1.1 同步与异步的区别:
- 同步:
是指程序中的任务必须要按照顺序执行,前一个任务完成之后,后一个任务才能开始。在同步操作中,调用者是必须等待操作完成并返回结果才能继续执行接下来的代码。这就表明了当存在某个操作需要耗费大量的时间比如说(
I/O操作,网络请求等任务),那么整个程序就会暂停等待这些任务处理完成。(这也正是出现异步的原因) - 异步: 是指它允许程序在等待耗时操作的同时继续执行其他任务。当发生一个异步操作的时候,它不会阻塞调用者的执行流程,而是继续执行后续代码。
1.2 事件循环与回调:
关于Javascript的事件循环机制的详细介绍,在玩转事件循环机制这一篇武功秘籍里面有详细的介绍。
1.3 回调地狱问题:
当然在异步操作的增多的时候,随着回调函数的嵌套层数增多时,就会出现回调地狱的问题,它会导致性能问题以及代码结构变得难以阅读和维护。并且高度耦合的回调函数会使得代码难以被重构或重用于其他场景。
下面举个例子玩一下:
setTimeout(() => {
console.log('我爱吃')
setTimeout(() => {
console.log('肉夹馍')
setTimeout(() => {
console.log('葱油大饼')
}, 1000)
}, 2000)
}, 3000)
2. Promise
2.1 Promise的概念:
既然遇到了回调地狱这个问题,我们就要去思考怎么去解决呢?
接下来就让我来介绍一下解决方案---Promise。Promise是一个对象,用于异步计算。它代表了一个最终会完成(或失败)的异步操作,并且会返回一个值。Promise的设计目的是为了解决回调地狱问题,使得异步代码的结构更加线性,易于理解和维护。
Promise有三种基本状态:
-
Pending(等待状态):这是
Promise的初始状态,表示异步操作尚未完成。在此阶段,Promise既没有被成功解析(fulfilled)也没有被失败拒绝(rejected)。 -
Fulfilled(完成状态):当异步操作成功完成时,
Promise会从pending状态转变到fulfilled状态。在这一状态下,Promise会携带一个值,这个值可以通过.then()方法访问。 -
Rejected(拒绝状态):如果异步操作失败,
Promise会从pending状态转变到rejected状态。在这一状态下,Promise会携带一个错误信息,这个错误信息可以通过.catch()方法捕获和处理。
2.2 Promise的优点:
Promise提供了几个方法来处理异步操作的结果:
.then():当Promise成功完成时调用的回调函数。可以接受两个参数,第一个参数是Promise成功时的回调,第二个参数(可选)是Promise失败时的回调。如果.then()中的函数返回一个新的Promise,那么链中的下一个.then()将等待这个新的Promise完成。.catch():专门用于处理Promise失败时的回调函数。它会捕获前面任何.then()中抛出的错误,或者Promise被reject时的错误。.finally():无论Promise最终是fulfilled还是rejected,都会执行的回调函数。它主要用于清理资源,如关闭文件句柄或释放内存。
2.3 Promise的创建与使用:
-
创建Promise实例
const myPromise = new Promise((resolve, reject) => { // 模拟异步操作 setTimeout(() => { try { const result = '异步操作成功'; resolve(result); // 成功时调用resolve } catch (error) { reject(error); // 失败时调用reject } }, 2000); });在这个例子中,我们创建了一个
Promise实例myPromise,它模拟了一个耗时2秒钟的异步操作。 如果异步操作成功,resolve函数会被调用并传递结果;如果操作失败或抛出错误,reject函数会被调用并传递错误。Promise对象构造函数是一个立即执行函数,但是它的resolve和reject方法是异步执行的- 当我们执行到
resolve()方法的时候,我们会把这个方法放到任务队列中,等待主线程的执行栈清空后,再从任务队列中取出这个方法来执行。 - 当我们执行到
reject()方法的时候,它会直接把错误信息抛出,然后把Promise对象的状态变为rejected。 Promise对象的状态一旦改变,就不会再变。
-
使用.then()和.catch()
.then()方法用于处理Promise成功的情况,可以接受两个参数,第一个是成功时的回调,第二个是可选的失败时的回调。如果.then()中的函数返回一个Promise,则下一个.then()会等待这个Promise完成。.catch()方法专门用于处理Promise失败的情况,它捕获前面任何.then()中抛出的错误,或者Promise被reject时的错误。
// 使用.then()和.catch()处理结果 myPromise .then((result) => { console.log('成功:', result); }) .catch((error) => { console.error('失败:', error); }); -
使用Promise.all与Promise.race方法:
-
Promise.all和Promise.race是处理多个Promise实例的两种常用方法,它们分别用于不同的场景。 -
Promise.all
Promise.all接受一个Promise数组作为参数,当所有Promise都完成(fulfilled)时,Promise.all返回的Promise才会完成,否则如果任何一个Promise失败(rejected),Promise.all返回的Promise也会立即失败。const promise1 = Promise.resolve('第一个Promise'); const promise2 = Promise.resolve('第二个Promise'); const promise3 = Promise.resolve('第三个Promise'); Promise.all([promise1, promise2, promise3]) .then((results) => { console.log('所有Promise完成:', results); }) .catch((error) => { console.error('至少一个Promise失败:', error); }); -
Promise.race
Promise.race同样接受一个Promise数组作为参数,但是它返回的Promise会在数组中的任何一个Promise完成或失败时立即完成或失败,这取决于最先完成或失败的那个Promise。 意思就是它会获取最先成功的Promise,不在乎结果怎么样,只要有一个成功就行,并且只会返回最先成功的那个Promise。const promiseA = new Promise((resolve) => setTimeout(resolve, 500, '慢速Promise')); const promiseB = Promise.resolve('快速Promise'); Promise.race([promiseA, promiseB]) .then((value) => { console.log('最先完成的Promise值:', value); }) .catch((reason) => { console.error('最先失败的Promise原因:', reason); });在上面的例子中,
Promise.race将返回promiseB的Promise,因为它会立即完成,而promiseA需要等待500毫秒。这使得Promise.race非常适合用于超时处理或在多个可能完成的异步操作中选择最快的结果。
-
-
Promise.all就适用于当一个操作可能需要多个接口的返回数据的时候。 -
Promise.race就适用于有好几个服务器提供的同样的服务,就可以使用Promise.race,哪个接口更快就用哪个。
3. Async/Await
3.1 Async/Await简介以及工作原理:
讲到async函数
- 当我们用
async关键字定义一个函数时,就表明这个函数为异步函数,被定义的这个函数的返回值是一个AsyncFunction对象的异步函数 - 当
async函数执行时,它会返回一个Promise对象,这个Promise对象会等待async函数执行完成,就算你没有明确的返回一个Promise,函数内部也会自动创建一个Promise对象,如果async函数中没有任何return语句,它将返回一个解析为undefined的Promise。 - 如果
async函数包含一个return语句,且返回的不是Promise,则该值会被封装进一个解析的Promise中。 - 如果
return语句返回一个Promise,则async函数的返回Promise将等待这个内部Promise的结果。
讲到await的话
首先它只能定义在async函数中。
await是用来等待Promise对象返回的结果
await会暂停async函数的执行,等待Promise对象返回结果,然后恢复async函数的执行。- 提高代码可读性,让代码看起来像是同步的。
- 如果
await不放在async函数中,我们无法控制何时暂停和恢复执行流,这样会导致程序逻辑混乱。- 因为
await会阻塞后面的代码,只有当Promise对象的状态变为resolved时,才会继续执行下面的代码 - 同时
await会返回Promise对象的状态值,如果Promise对象的状态为rejected,await会抛出异常 - 其次,
await后面只能跟Promise对象,不能跟普通值,返回Promise对象的处理结果。 - 如果等待的不是
Promise对象,则返回该值本身。 - 所以当await操作符后面的表达式是一个
Promise的时候,它的返回值,实际上就是Promise的回调函数resolve的参数。
- 因为
3.2 Async/Await的使用:
关于使用的话,直接来分析几道题即可
1.例一:
async function async1() {
console.log('async1 start')
await async2()
console.log('async1 end')
}
async function async2() {
console.log('async2')
}
console.log('script start')
setTimeout(function () {
console.log('setTimeout')
}, 0)
async1();
new Promise(function (resolve) {
console.log('promise1')
resolve();
}).then(function () {
console.log('promise2')
})
console.log('script end')
来吧,少侠告诉我你的答案 正确的答案:
script start
async1 start
async2
promise1
script end
async1 end
promise2
setTimeout
所以是为什么呢?
我刚开始一直对这个Promise对象函数和async1以及async2中的打印是很模糊的。
-
现在我们来具体分析一下:
-
首先我们打印的肯定是
script start -
遇到
setTimeout函数,会将setTimeout放到下一次事件循环中 -
执行
async1函数,遇到同步代码,打印async1 start -
遇到
await,会暂停async1函数的执行,所以async1 end不会打印,并等待await后面的Promise对象返回结果 -
将
async2函数放入到微任务队列中 -
执行
Promise对象函数,打印promise1 -
遇到
Promise对象,因为Promise的构造函数是立即执行的,所以我们会打印promise1 -
遇到
Promise的回调函数,把then()放入到微任务队列之中 -
继续执行同步代码
script end -
接着
js引擎会检查当前事件循环中是否有微任务,发现有微任务 -
执行微任务,打印
async2和async1 end -
继续执行微任务打印
promise2 -
执行下一次事件循环,打印
setTimeout
-
-
所以输出结果为
script start async1 start promise1 script end async2 async1 end promise2 setTimeoutok,少侠,我犯下了错误,当我发现打印结果是
script start async1 start async2 promise1 script end async1 end promise2 setTimeout-
我才发现我对
async和await的理解还是不够深入。 -
后面我发现其实在执行
async2的时候其实跟我的分析是有出入的,其实
await async2()它不会被放在任务队列之中的。(我刚开始看到async1 end在promise1后面打印我还以为await async2();console.log('async1 end');被放到任务队列之中去了) -
await关键字的作用是暂停当前的异步函数(在本例中是async1)的执行,直到async2函数返回的Promise变为resolved或rejected -
之后继续执行
async1函数的剩余部分。 而为什么promise1会在async1 end之前打印,是因为在等待async2()返回的Promise变为resolved之前,async1函数的执行被暂停了, -
所以会先执行
Promise的构造函数中的同步代码,然后才继续执行async1函数的剩余部分。
所以快让我们重新再来分析一波吧:
-
打印
script start:这是最开始的同步代码,它会被立即执行。 -
setTimeout函数:确实,setTimeout被放置在宏任务队列中,将在当前执行栈清空后,在下一轮事件循环中执行。 -
执行
async1函数:async1函数开始执行,首先打印async1 start。 -
await async2():遇到 await 关键字时,async1的执行将暂停,直到async2函数返回的Promise解析完成。但是,async2函数的执行是立即的,它打印async2,然后返回一个默认的fulfilledPromise(因为async2没有异步操作)。重要说明:
async2函数并没有被放入微任务队列中。async2的执行是同步的,直到它返回一个Promise,这个Promise几乎立即完成,因为async2没有异步操作。await关键字的作用是等待这个Promise完成,然后恢复async1函数的执行。 -
Promise对象函数执行:new Promise(function (resolve) { ... })创建了一个新的Promise。Promise构造函数内的代码是同步执行的,所以promise1被打印出来。 -
Promise的回调函数:.then(function () { ... })注册了一个回调函数,这个回调函数会被放入微任务队列中,等待当前执行栈清空后执行。 -
继续执行同步代码:在
async1和Promise构造函数执行之后,script end被打印出来,这是同步代码,所以它在setTimeout和微任务之前执行。 -
微任务执行:一旦当前执行栈清空,事件循环会检查微任务队列。
首先,
async1函数会继续执行,打印async1 end,这是因为await async2()等待的Promise已经完成。然后,
.then回调函数被执行,打印promise2。
所以的所以,因此,
async1和async2不会直接进入微任务队列。它们的行为更像是包含了可能的异步操作的函数,这些异步操作通过Promise和await关键字来实现。 只有当Promise的回调函数(如.then()和.catch())才被加入到微任务队列中等待执行。2.例二:又是一道经典的题目
// 第一块代码 Promise.resolve().then(() => { console.log(0); return Promise.resolve(4); }).then((res) => { console.log(res) }) // 第二块代码 Promise.resolve().then(() => { console.log(1); }).then(() => { console.log(2); }).then(() => { console.log(3); }).then(() => { console.log(5); }).then(() => { console.log(6); })好好好,这个题居然最后输出的结果是
0 1 2 3 4 5 6好吧,那我们只能狠狠地来分析分析了
-
这里面涉及了
Promise对象的链式调用,这个过程是十分复杂的 -
首先在这里,我们要知道每一次
Promise对象的静态resolve()方法和Promise实例的then()方法都会返回一个新的Promise对象实例,也就是return new Promise()也正是因为这个原来才能实现Promise的链式调用
-
Promise.resolve(4)本身会产生一个新的promise实例,接着它作为值传给了then方法的成功回调(如果失败的话也会执行失败的回调)。 -
由于值是
Promise实例,所以会调用值的then方法, 所以又产生了一层新的Promise实例,这也就导致4在3后。 -
意思就是当产生Promise实例的时候,会将这个事件放到微任务队列,因为它会等待返回的解决(fulfill)时的回调或者拒绝(reject)时的回调。
所以上面第一块的代码等价于
Promise.resolve().then(() => { console.log(0); return 4; }) .then() .then() .then((res) => { console.log(res) })- 如果上面的这个讲解还是不清楚的话,请你结合下面我讲的东西,帮助理解。这里想介绍一下
Promise的回调的时候,会发现传入的参数和调用的方式的不同时,它们是有区别的。
3.不同返回类型的区别
虽然下面的代码1,2,3最后输出的结果都是4,但是它们在调用上面是有区别的
-
代码1:
new Promise(resolve => { resolve(Promise.resolve(4));//resolve了一个Promise }) .then((res) => { console.log(res) })在这段代码中,我们创建了一个新的
Promise,并在resolve函数中立即解析了一个包含数字4的已解析Promise。 当外部Promise解析时,它会解析为内部Promise的解析值,即4。因此,.then()回调将接收到数字4并将其打印出来resolve()方法中放入的时一个Promise对象。- 首先第一次是里面的
Promise对象调用了静态方法resolve创建了一个还在pending的Promise实例但是传递的值是可以立即拿到的接着Promise状态会变成fulfilled。 - 然后外层要传递这个值的内容,会重新创建一个Promise实例并调用
then()方法 - 这样就会实现两次加入微任务队列的操作
- 首先第一次是里面的
-
代码2:
Promise.resolve().then(() => { return Promise.resolve(4);//return了一个Promise }) .then((res) => { console.log(res) })-
在这段代码中,我们从
Promise.resolve()开始,这创建了一个已解析的Promise。 -
然后我们在
.then()回调中返回另一个已解析的Promise,其解析值为4。 -
当这个内部
Promise解析时,其解析值4将传递给下一个.then()回调,最终打印出来。 -
这里的
return了一个Promise和 上面的方法是类似的解析过程
-
-
代码3:
Promise.resolve().then(() => { return 4;//return了一个Number类型的4 }) .then((res) => { console.log(res) })-
在这段代码中,我们同样从
Promise.resolve()开始,创建了一个已解析的Promise。 -
但是,这次在
.then()回调中,我们直接返回了一个数值4。 -
由于返回的不是一个
Promise,.then()会直接将这个值传递给链中的下一个.then(),最终也将打印出4。
-
4.例三:让我幡然醒悟的一道题目
-
原以为下面最后的输出结果是
0 1 2 3 5 6 4 -
结果结果为
0 1 2 3 4 5 6 -
发现其实不管嵌套多少层的
Promise,只要它不涉及往下面传递内容时,是不会产生微任务的,所以这里的return Promise.resolve(Promise.resolve(Promise.resolve(4)))等价于return Promise.resolve(4)。终于发现能够理解上面的内容了。 -
因为在最外层的resolve下面Promise状态变为fulfilled的过程并不需要等待。
Promise.resolve().then(() => { console.log(0); return Promise.resolve(Promise.resolve(Promise.resolve(4))) }) .then(res => { console.log(res); })
-
3.3 错误处理:
对于错误处理肯定就是使用try...catch语句了,用法也十分简单。
async function myAsyncFunction() {
try {
const result = await new Promise((resolve, reject) => setTimeout(() => reject(new Error('失败')), 1000));
} catch (error) {
console.error(error.message); // 输出:失败
}
}
4. 最佳实践与陷阱
- 使用Promise链: 使用
then和catch方法来构建Promise链,这有助于保持代码的顺序性和清晰度。尽量避免嵌套的回调地狱。 - 错误处理: 总是在
Promise链中包含catch块来处理可能发生的错误。不要忽略错误,确保每个异步操作都有适当的错误处理逻辑。 - 使用
async/await: 多使用async/await语法,因为它使异步代码看起来更像同步代码,提高可读性。但是要注意,async/await只能在async函数内使用。 - 未处理的Promise拒绝: 忽略
Promise的catch可能导致未处理的Promise拒绝。确保每个Promise都有适当的错误处理。 - 资源泄露: 忘记关闭资源,如文件描述符、数据库连接,会导致资源泄露,最终可能耗尽系统资源。
- 死锁和竞争条件: 在复杂的异步逻辑中,不正确的同步点可能导致死锁或竞争条件。使用原子操作和互斥锁来保护共享资源。
- 回调地狱: 避免过多的嵌套回调,因为这样会使会使代码难以理解和维护。使用
Promise链或async/await来简化代码结构。