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 setTimeout
ok,少侠,我犯下了错误,当我发现打印结果是
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
,然后返回一个默认的fulfilled
Promise(因为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
来简化代码结构。