🌙 1. 认识async/await函数
还记得学习promise时候的脑筋急转弯吗?“把牛放进冰箱里,要几步?” (opens new window)
// 第一步,打开冰箱
function open(){
setTimeout(()=>{
console.log('打开冰箱');
return 'success';
}, 1000)
}
// 第二步,放牛进去
function settle(){
setTimeout(()=>{
console.log('放牛进去');
return 'success';
}, 3000)
}
// 第三步,关上冰箱
function close(){
setTimeout(()=>{
console.log('关上冰箱');
return 'success';
}, 1000)
}
function closeCow(){
open();
settle();
close()
}
closeCow();
//"打开冰箱"
//"关上冰箱"?
//"放牛进去"?
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
前面使用prmoise解决如下:
let closeCow = new Promise((resolve, reject) => {
resolve();
})
closeCow.then(open()).then(settle()).then(close());
// 打开冰箱
// 关上冰箱?
// 放牛进去?
2
3
4
5
6
7
8
我们知道Promise
属于微任务,setTimeout
属于宏任务,在主代码块扫描过后,执行下一个任务的时候,会先把所有的微任务处理完,再处理宏任务,所以Promise
(.then
返回的就是Promise对象)会优先于setTimeout
执行,但是当setTimeout
里面等待时间更长的时候,显然等待时间短的先执行。也就是说,三个.then
中的setTimeout
依次进入宏任务队列里面等待执行。
这也就导致了上面的错误结果。
其实Promise
和setTimeout
都是异步操作的解决方案,这里错误的将二者混用了,导致第三个.then
先于第二个.then
执行,所以在使用.then
链式调用的时候,最好不要在.then
里面进行异步操作,否则可能会导致后面的then
先执行。
下面去掉.then
中的异步操作setTimeout
:
// 第一步,打开冰箱
function open(){
console.log('打开冰箱');
return 'success';
}
// 第二步,放牛进去
function settle(){
console.log('放牛进去');
return 'success';
}
// 第三步,关上冰箱
function close(){
console.log('关上冰箱');
return 'success';
}
Promise.resolve().then(open).then(settle).then(close);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
我们还可以使用function*
生成器:
function* genCloseCow (){
yield open();
yield settle();
return close();
}
let closeCow = genCloseCow();
closeCow.next();
close.next();
closeCow.next();
2
3
4
5
6
7
8
9
10
使用async/await
方式如下:
async function asyncCloseCow(){
await open();
await settle();
await close();
}
asyncCloseCow(); // 返回值 Promise {<resolved>: undefined}
2
3
4
5
6
7
如果,就是要保留
setTimeout
方法,又该怎么实现呢?请看后文。
其实async/await
就是function*
和Promise
二者的结合:
async
函数就是将 Generator 函数的星号(*
)替换成async
,将yield
替换成await
,并且返回Promise
对象,可以链式调用.then
方法。
- 内置执行器:
function*
函数必须调用.next
方法才能被唤醒执行,async/await
可以像普通函数一样调用执行。 - 语义化更好:
async/await
中async
表示函数里有异步操作,await
表示后面的代码要等待执行。而function*
会让人迷惑。 - 适用性更强:
function*
的yield
命令后面只能跟Thunk函数
(opens new window)(传名调用)或Promise对象
,而async/await
的await
命令后面可以是Promise 对象
和原始类型的值
(数值、字符串和布尔值,但这时会自动转成立即 resolved 的 Promise 对象)。
🌙 2. async/await函数使用形式
JS中由于函数的形式有很多种,故async/await
的形式也有多种:
函数申明式
async function foo(){}
1函数表达式
const foo = async function() {};
1箭头函数式
const foo = async () => {};
1对象方法
let obj = { async foo() {} }; obj.foo().then(res => console.log(res));
1
2Class 方法
class Foo { constructor(){}; async foo(){}; } const f = new Foo(); f.foo();
1
2
3
4
5
6
7
🌙 3. async/await详解
🌙 3.1 async/await返回值
前面知道async/await
方法执行之后返回一个Promise对象,那么函数内部的return
呢?
async
函数内部return
语句返回的值,将被隐式地传递给Promise.resolve
:
async function hello() {
return 'hello world!';
}
let h = hello(); // h --> Promise {<resolved>: "hello world!"}
h.then(res => console.log(res)); // hello world!
2
3
4
5
6
返回值隐式的传递给
Promise.resolve
(opens new window),并不意味着return await promiseValue;和return promiseValue;
在功能上相同:
async function bar1(){
return foo;
}
async function bar2(){
return await foo;
}
2
3
4
5
6
return foo;
和return await foo;
有一些细微的差异:
return foo;
不管foo
是promise还是rejects都将会直接返回foo。相反地,
如果foo
是一个Promise
(opens new window),return await foo;
将等待foo
执行(resolve)或拒绝(reject),如果是拒绝,将会在返回前抛出异常。
🌙 3.2 async/await状态和错误处理
既然async/await
方法执行之后返回一个Promise对象,那么这个Promise对象状态又是如何变化的呢?
async
函数返回的 Promise 对象,必须等到内部所有await
命令后面的 Promise 对象执行完,才会发生状态改变,除非遇到return
语句或者抛出错误。也就是说,只有async
函数内部的异步操作(Promise异步操作 ,不是setTimeout)执行完,才会执行then
方法指定的回调函数。
let hello = () => Promise.resolve('hello await');
async function sayHi() {
let hello = await hello();
return hello;
}
sayHi().then(res => console.log(res));
2
3
4
5
6
7
任何一个await
语句后面的 Promise 对象变为reject
状态,那么整个async
函数都会中断执行:
async function f() {
await Promise.reject('出错了');
await Promise.resolve('hello world'); // 不会执行
}
2
3
4
await
命令后面的 Promise 对象如果变为reject
状态,则reject
的参数会被catch
方法的回调函数接收到:
async function f() {
await Promise.reject('出错了');
}
f()
.then(v => console.log(v))
.catch(e => console.log(e))
// 出错了
2
3
4
5
6
7
8
有时,我们希望即使前一个异步操作失败,也不要中断后面的异步操作。这时可以将第一个await
放在try...catch
结构里面,这样不管这个异步操作是否成功,第二个await
都会执行。
async function f() {
try {
await Promise.reject('出错了');
} catch(e) {
}
return await Promise.resolve('hello world');
}
f()
.then(v => console.log(v))
// hello world
2
3
4
5
6
7
8
9
10
11
另一种方法是await
后面的 Promise 对象再跟一个catch
方法,处理前面可能出现的错误。
async function f() {
await Promise.reject('出错了')
.catch(e => console.log(e));
return await Promise.resolve('hello world');
}
f()
.then(v => console.log(v))
// 出错了
// hello world
2
3
4
5
6
7
8
9
10
🌙 3.3 await 关键字
这个关键字是async/await
的核心,后面可以跟Promise 对象
和原始类型的值
(数值、字符串和布尔值,但这时会自动转成立即 resolved 的 Promise 对象)或者 thenable对象
(实现了Promise的then方法)。
正常情况下,await
命令后面是一个 Promise 对象,返回该对象的结果。如果不是 Promise 对象,就直接返回对应的值。
async function f() {
// 相当于return 123
return await 123;
}
f().then(res => console.log(res));
2
3
4
5
sleep睡眠函数:
function sleep(time) {
return new Promise(resolve => {
setTimeout(resolve, time);
})
}
(async function foo() {
for(let i=0; i<5;i++){
console.log(i);
await sleep(1000);
}
})()
2
3
4
5
6
7
8
9
10
11
12
下面代码中,await
命令后面是一个Sleep
对象的实例。这个实例不是 Promise 对象,但是因为定义了then
方法,await
会将其视为Promise
处理。
class Sleep {
constructor(timeout) {
this.timeout = timeout;
}
// 定义then方法
then(resolve, reject) {
const startTime = Date.now();
setTimeout(
() => resolve(Date.now() - startTime),
this.timeout
);
}
}
(async () => {
const sleepTime = await new Sleep(1000);
console.log(sleepTime);
})();
// 1000
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
前面"把牛关进冰箱"案例里面使用setTimeout
导致顺序错乱问题,下面来解决:
// 打开冰箱
function open(){
return new Promise(resolve => {
setTimeout(()=>{
console.log('打开冰箱');
resolve(); // 等待时间结束,再提升状态
}, 1000);
})
}
// 放牛进去
function settle(){
return new Promise(resolve => {
setTimeout(()=>{
console.log('放牛进去');
resolve(); // 等待时间结束,再提升状态
}, 3000);
})
}
// 关闭冰箱
function close(){
return new Promise(resolve => {
setTimeout(()=>{
console.log('关闭冰箱');
resolve(); // 等待时间结束,再提升状态
}, 1000);
})
}
async function closeCow() {
await open();
await settle();
await close()
}
closeCow();
// 打开冰箱
// 放牛进去
// 关闭冰箱
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
这里使用setTimeout
能成功解决前面的问题的原因:
- 三个方法返回的都是
promise
对象。 await
后面跟Promise对象的时候,await
语句后面的代码必须等待当前Promise状态提升为resolved
状态(reject
报错之后,后面的await不会执行),才会继续执行。- 三个方法均把
resolve
方法放在setTimeout
里面,即必须等待时间结束,当前Promise状态才会被提升。
通过这个方式,我们就严格保证了三个方法按照顺序执行了异步操作(setTimeout
)。
但是,当我们多个方法不需要严格的执行顺序的时候,我们其实可以这样并发执行:
(async function () {
let foo = await getFoo();
let bar = await getBar();
// 写法一
let [foo1, bar1] = await Promise.all([getFoo(), getBar()]);
// 写法二
let fooPromise = getFoo();
let barPromise = getBar();
let foo2 = await fooPromise;
let bar2 = await barPromise;})()
2
3
4
5
6
7
8
9
10
11
12
13
🌙 4. async/await实现原理
async 函数的实现原理,就是将 Generator 函数和自动执行器,包装在一个函数里。
async function fn(args) {}
// 等同于
function fn(args) {
return spawn(function* (){});
}
2
3
4
5
所有的async
函数都可以写成上面的第二种形式,其中的spawn
函数就是自动执行器:
function spawn(genF) {
return new Promise(function(resolve, reject) {
const gen = genF();
function step(nextF) {
let next;
try {
next = nextF();
} catch(e){
return reject(e);
}
if(next.done){
return resolve(next.value)
}
Promise.resolve(next.value).then(function(v){
step(function(){return gen.next(v);});
}, function(e) {
step(function() {return gen.throw(e);});
})
step(function(){return gen.next(undefined);});
}
})
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
🌙 5. async/await测试题
🌙 5.1 测试一
console.log(1);
async function asyncfn1(){
console.log(2);
await asyncfn2();
console.log(3);
};
setTimeout(() => {
console.log('setTimeout')
}, 0)
async function asyncfn2(){
console.log(4)
};
asyncfn1();
console.log(5);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
查看答案
1 2 4 5 3 setTimeout
🌙 5.2 测试二
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')
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
查看答案
script start async1 start async2 promise1 script end promise2 async1 end setTimeout
详细解读
1.执行同步代码script start
2.遇到setTimeout,推入宏任务队列
3.执行async1()async1 start
4.遇到await 执行右侧表达式后让出线程,阻塞后面代码async2
5.执行promise中的同步代码promise1
6.将.then()推入微任务队列向下执行同步代码script end
7.同步代码执行完毕,执行所有微任务队列中的微任务promise2
8.微任务执行完毕,执行await后面的代码async1 end
9.带 async 关键字的函数,它使得你的函数的返回值必定是 promise 对象或undefinedasync return
10.带 async 关键字的函数,执行后会自动打印undefinedundefined
11.开始下一轮evenloop,执行宏任务队列中的任务setTimeout