ES6-async&await语法糖

2019/10/6 JSES6async

🌙 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();

//"打开冰箱"
//"关上冰箱"?
//"放牛进去"?
1
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()); 
// 打开冰箱
// 关上冰箱?
// 放牛进去?
1
2
3
4
5
6
7
8

我们知道Promise属于微任务,setTimeout属于宏任务,在主代码块扫描过后,执行下一个任务的时候,会先把所有的微任务处理完,再处理宏任务,所以Promise.then返回的就是Promise对象)会优先于setTimeout执行,但是当setTimeout里面等待时间更长的时候,显然等待时间短的先执行。也就是说,三个.then中的setTimeout依次进入宏任务队列里面等待执行。

这也就导致了上面的错误结果。

其实PromisesetTimeout都是异步操作的解决方案,这里错误的将二者混用了,导致第三个.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);
1
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();
1
2
3
4
5
6
7
8
9
10

使用async/await方式如下:

async function asyncCloseCow(){
    await open();
    await settle();
    await close();
}

asyncCloseCow(); // 返回值 Promise {<resolved>: undefined}
1
2
3
4
5
6
7

如果,就是要保留setTimeout方法,又该怎么实现呢?请看后文。

其实async/await就是function*Promise二者的结合:

async函数就是将 Generator 函数的星号(*)替换成async,将yield替换成await,并且返回Promise对象,可以链式调用.then方法。

  • 内置执行器:function*函数必须调用.next方法才能被唤醒执行,async/await可以像普通函数一样调用执行。
  • 语义化更好:async/awaitasync表示函数里有异步操作,await表示后面的代码要等待执行。而function*会让人迷惑。
  • 适用性更强:function*yield命令后面只能跟Thunk函数 (opens new window)传名调用)或Promise对象,而async/awaitawait命令后面可以是 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
    2
  • Class 方法

    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!
1
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;
}
1
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));
1
2
3
4
5
6
7

任何一个await语句后面的 Promise 对象变为reject状态,那么整个async函数都会中断执行:

async function f() {
  await Promise.reject('出错了');
  await Promise.resolve('hello world'); // 不会执行
}
1
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))
// 出错了
1
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
1
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
1
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));
1
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);
    }
})()
1
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
1
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();
// 打开冰箱
// 放牛进去
// 关闭冰箱
1
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;})()
1
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* (){});
}
1
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);});
        }
    })
}
1
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);
1
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')
1
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 对象或undefined async return
10.带 async 关键字的函数,执行后会自动打印undefined undefined
11.开始下一轮evenloop,执行宏任务队列中的任务 setTimeout