傲娇大少之——【咱也聊一下 async 和 await】

先来一碗毒鸡汤润润喉:
经常失恋的人总是会说:没关系,我相信肯定会有一个懂我的人一直在等着我!
阎王说:吼吼,没想到是我吧!

不求甚解之 ---- async 和 await

咱先从字面意思看:
async:异步
await:等待
所以,这东西就是为了实现异步被设计出来的。

async和await是ES2017新增的语法,async函数呢,本质上其实就是Generator函数的语法糖。

既然如此,咱就先简单说一下Generator函数。

Generator函数

Generator函数,也叫生成器函数,是ES2015规范(ES6)中引入的。

大家知道JavaScript是单线程的,在一个函数中,除非遇到return,否则都会顺序执行到函数结束。生成器函数可以打破这个规则。

记住生成器函数的特点:

  • 定义函数时,function后边多了个星号“*”;
  • 函数体内部,多了个yield表达式;

执行机制:
调用 Generator 函数和调用普通函数一样,在函数名后面加上()即可,但是 Generator 函数不会像普通函数一样立即执行,而是返回一个指向内部状态对象的指针,调用遍历器对象的 next 方法,指针会在函数内部继续执行,遇到下一个yeild停,再调用next,指针再继续执行。

举个例子:

function* gen(){
    console.log("one");
    yield '1'; // 注:yield后的值 '1',会作为返回对象的 value 属性值
    console.log("two");
    let a = yield '2';
    console.log("three" + a);
    return '3';
}

let g = gen(); // 生成生成器函数的实例
console.log(g.next()); // one  {value: "1", done: false}
console.log(g.next(3333)); // two  {value: "2", done: false}
console.log(g.next()); // three3333  {value: "3", done: true}
console.log(g.next()); // {value: undefined, done: true}
console.log(g.next()); // {value: undefined, done: true}

从例子中可以发现,生成器函数就是next一下,就执行函数,碰到yield,就停。然后给next()的返回个对象,这个返回对象有两个属性value和done,总结一下:

  • value:yield后边的值,可以根据这个查看执行到那个yield了;
  • done:这个函数是否执行完成了,false代表未执行完成,true代表执行完成。
  • 函数执行到底后,再继续调用next,会返回 {value: undefined, done: true};
  • next可以传参;
  • 调用实例的return方法,可以提前结束Generator函数(g.return());
  • yield后面加星号yield* 表达式用来在一个 Generator 函数里面 执行 另一个 Generator 函数

常用方法:
正常开发过程中,next肯定是在循环里面调用的,否则没啥意思。
Generator 函数运行时生成的是一个 Iterator 对象,因此,可以直接使用 for…of 循环遍历,且此时无需再调用 next() 方法,例如:

for(let item of gen()) {
    console.log(item); // 注: 一旦done属性值为true时,for-of 循环就会终止,包括该对象也不会返回
}

async 和 await

回归真题,了解了Generator函数后,咱们再来说async函数。上边也说了,async就是Generator的语法糖。理解下大概就是:
async函数等同于 Generator 函数,yield等同于成await。然后在Generator函数的基础上进行了功能的改进:

  • async函数的await可以等待异步处理完成后自动触发,无需像Generator函数一样靠next去一步步触发;
  • 语义相对更清晰,async 和 await 怎么看着都比 Generator 和 yield好理解。

用我的大白话说就是,async函数和Generator函数一样,没有await配对,就是个普通函数,有了await才有了意义。
await后面一般跟的是promise对象。await后面跟promise对象语义上很好理解,大概就是await说:我等你呀。promise回答:好呀!promise的then回调函数执行成功后,await会自动的继续执行。
await后面跟其它的像变量或表示之类的就没有太大的意义,因为不是异步的处理,await等待的时间太短。感觉上就像await说:开始了吗?后边说:已经结束了!

算了,说多了都是泪,概念性的东西,不理解真的是说不清楚。咱还是写个例子吧!
写法:

function ppp(param) {
    return new Promise((resolve, reject) =>{
         setTimeout(() => {
             resolve(param);
         }, 1000);
     });
 }

 async function aaa () {
     console.log('start');
     const num = await 213;
     console.log(num);
     const a = await ppp('Let\'s go!');
     console.log(a);
     const b = await ppp('Have launch!');
     console.log(b);
     const c = await ppp('Have dinner!');
     console.log(c);
 }
 
 aaa();
 console.log('hahahha!');

执行结果:(注:后边三个数据,每隔一秒钟打印一个。)
在这里插入图片描述
为了便于理解记忆,咱再往简单了说哈!

  • async就是一个函数的声明标识,用处就是使里面的await语句变的有意义。
  • await后边一般跟的是一个promise对象,promise执行成功后,将resolve值作为返回值返回给await。
  • await下边语句的执行,取决于这个promise的then函数是否能成功回调。
  • 当然了,如果promise执行失败了,那么这个async函数会异常结束。

我觉得了解到这里,对async和await的基本概念就理解的差不多了,再说点需要注意的地方吧!

1. await的循环

正常我们开发的过程中,能用到await的情况,基本都是多异步的情况,所以await大部分都是写在循环语句中的。那么就要注意了,这个循环到底该怎么写?

看下边这个例子:

let arr = ['a', 'b', 'c'];
async function fun() {
    arr.forEach((item) => {
        await console.log(item);
    });
}
fun();

报错了哈:
在这里插入图片描述
分析:
上边说过,await只有在函数声明async才有效,那么看上面例子中的await到底在哪个函数体中?没错,是循环里面的那个箭头函数中,而这个箭头函数肯定不是async函数,所以报错了。
所以forEach不适用与await的循环,我们要用for…of,改成下边这样就不报错了哈。

async function fun() {
   	for (item of arr) {
   		await console.log(item);
   	}
}

2. 多并发.
因为await是一步一步顺序执行的,如果希望多个请求并发执行,可以用Promise.all()。
举例:

function aaa(param, time) {
   return new Promise((resolve) =>{
        setTimeout(() => {
            console.log(param);
            resolve(param);
        }, time);
    });
}
async function fun() {
    console.log('start');
    let a = await aaa('have 1', 3000);
    console.log(a);
    let b = await aaa('have 2', 1000);
    console.log(b);
    let c = await aaa('have 3', 500);
    console.log(c);
}

fun();

执行结果,肯定是按顺序,3秒后打印have1;4秒后打印 have 2;4.5秒后打印have 3。
在这里插入图片描述
如果想要三个异步同时执行,可以使用Promise.all,改成以下这种方式

async function fun() {
   let results = await Promise.all([aaa('have 1', 3000), aaa('have 2', 1000), aaa('have 3', 500)]);
    console.log(results);
}

在这里插入图片描述
3. async函数声明方式
async声明有以下几种写法:

async function fun() {}

const fun = async function () {}; // 具名函数

const fun = async () => {}; // 箭头函数

class parent{
  constructor() {
  }
  async getAvatar() { // Class里面的方法
  }
}

4. async函数的return
之前咱们说过,async返回的是一个promise对象。
比如随便定一个有返回值的async函数:

async function fun () {
	return 'hello!';
}
console.log(fun());

看结果:
在这里插入图片描述
返回的确实是个promise对象哈!

那么改成这样,就能弹出来hello了:

fun().then(res => {console.log(res);});

5. try…catch
已知await后跟的都是promise对象,而promise对象有三个状态:pending,fulfilled,rejected。如果promise状态变成了rejected,代表执行失败了,那么await是不会继续执行的,这个时候async函数就会产生错误中断。
所以我们在写的时候,最好把代码写在try…catch中。可以捕获异常,后面的代码还会继续执行。

async function fun() {
    await aaa('have 1', 3000);
    try {
        await Promise.reject('我出错了!');
    } catch (e) {
        console.log(e);
    }
    await aaa('have 3', 500);
}

拓展

常见面试题:

setTimeout、Promise、Async/Await的区别?
答:
setTimeout:定时器,异步延时触发,定时器结束后会在宏任务队列中等待调用栈调用执行。
Promise:promise是自执行函数,但是其中的then是异步的,所以promise内部程序执行完成后,会在微任务队列中等待调用栈执行then函数的内容完成回调。
Async/Await:async函数会返回个promise对象,相当于自执行函数,但是如果语句中含有await,await执行完成后面的表达式后,会让出线程,同时在微任务队列中等待调用栈回调后执行async函数后面的语句。

解析:

我的第一想法就是这道题考的肯定是事件循环和回调队列的区别,那么需要知道的知识点如下:

  • JavaScript是单线程的:所有的异步回调都需要在等待主进程的同步事件执行完成后(即JS引擎的调用栈空出来后),再执行回调。
  • 事件循环机制:监控调用栈和回调队列,一旦调用栈空了,就会将回调队列中的任务放入调用栈中。
  • 回调队列有两种:微任务队列和宏任务队列,JavaScript规定先调用微任务队列的回调,后调用宏任务队列的回调。
  • promise函数的 then() 回调会在微任务队列中等待执行。
  • setTimeout,setInterval定时器会在宏任务队列中等待执行。

了解了这些知识点,问题就迎刃而解啦。如果还不明白的,推荐看这个大神的文章,我觉得写的挺好的:https://github.com/xitu/gold-miner/blob/master/TODO/how-javascript-works-event-loop-and-the-rise-of-async-programming-5-ways-to-better-coding-with.md

终极变态测试题:

看这段代码,写出你认为正确的结果。

console.log(1);
setTimeout(() => {
    console.log(2);
    new Promise(resolve => {
        console.log(3);
        resolve();
    }).then(() => {
        console.log(4);
    });
});
console.log(5);
let p1 = new Promise((resolve) => {
    console.log(6);
    resolve(6);
});
let p2 = new Promise((resolve) => {
    setTimeout(() => {
        console.log(7);
        resolve(7);
    }, 0);
});
async function fun2() {
    try {
        console.log(8);
        let aa = await Promise.all([p1, p2]);
        console.log(aa);
        console.log(9);
    } catch (e) {
        console.log(e);
    }
}
async function fun3() {
    setTimeout(() => {
        console.log(10);
    });
    console.log(11);
}
async function fun1() {
    console.log(12);
    await fun2();
    console.log(13);
}
fun1();
async function fun4() {
    await fun3();
    console.log(14);
}
fun4();
new Promise((resolve) => {
    console.log(15);
    resolve();
}).then(() => {
    console.log(16);
});

这是我自己综合了一些面试题总结后出的一道题,我觉得挺变态的,能做对,就说明,你对这东西已经掌握啦!

先看结果:
在这里插入图片描述

不知道你对没对,我第一次反正做错了,自己挖坑把自己埋得死死的……

分析开始!

咱们模拟一下JS引擎的执行顺序哈!

  1. 第一步,开始顺序执行哈,第一个打印1,毋庸置疑。
    执行结果: 1
  2. 第二步,碰到setTimeout,尽管它的延时时间为0哈,也给我乖乖到宏任务队列中排队去!咱给它起个名字,省的弄混了,就叫timer2吧,因为第一句是打印2,到时候咱们好找。所以到此:
    执行结果: 1
    宏任务队列:timer2
  3. 第三步,打印5,没毛病,到此结果:
    执行结果: 1,5
    宏任务队列:timer2
  4. 第四步,一个promise对象,大家知道promise是个自执行函数哈,所以什么都不用想,执行它,打印6。此时的执行结果:1,5,6。
    然后碰到resolve了,resolve固定promise对象的执行结果,所以到此,p1执行完成,resolve值为6,状态变为fulfilled。
    执行结果: 1,5,6
    宏任务队列:timer2
  5. 第五步,又是一个promise对象哈,又是一个自执行函数,还是直接执行它!但是不同的是里面是个定时器,虽然是包含在自执行函数中的定时器,那它也是个定时器呀,所以,去排队,咱给这个定时器起名,叫timer7哈(不要纠结名字,咱们只是为了便于查找)。所以这个promise对象状态一直为pending。
    执行结果: 1,5,6
    宏任务队列:timer2 => timer7
  6. 第六步,继续往下看,定义了三个函数,只是定义,没有调用,所以不执行,咱就不管它。
  7. 第七步,fun1(),调用了一个函数,所以咱们跳到fun1函数体内部去,第一句,打印12,没问题哈。第二句await fun2(),那咱们就再到fun2函数中去看看呗,第一句打印8,也没毛病哈。
    然后重点来了let aa = await Promise.all([p1, p2]);这句话的意思不得了了,要等p1,p2全都执行完成才行。
    咱们知道p1状态已经变成了fulfilled,执行完成;但是p2里面的定时器还在宏任务队列中排队呢,所以p2的状态还是pending。
    上边说过,await想要继续执行的话,相当于await后边的promise对象执行成功后的then回调。也就是说,我们需要Promise.all执行成功的then回调。
    我们知道Promise.all也是个promise对象,但是由于p2还没执行完成,还在pending。所以Promise.all对象也在相当于pending状态,它的then回调函数此时还没资格到微任务队列中去排队。
    所以await还得等待,fun2函数执行中断。同理,fun1函数也中断了,所以到此:
    执行结果: 1,5,6,12,8
    宏任务队列:timer2 => timer7
    待执行函数:fun1(),fun2()
  8. 第八步,继续顺序执行哈,调用fun4(),咱们跳到fun4函数体里面,发现第一句话是await fun3(),所以咱再进到fun3函数体里面去,发现里面是两句话,显示一个定时器,然后是一个console。所以定时器呢去排队,console11正常打印。那么此时:
    执行结果: 1,5,6,12,8,11
    宏任务队列:timer2 => timer7 => timer10
    待执行函数:fun1(),fun2(),fun3(),fun4()
  9. 第九步,fun3()执行完成,回到fun4函数中,fun3也是个async函数,所以它返回的是个promise对象,所以想要执行await fun3后续的代码等同于调用promise的then回调,所以这里产生了个微任务,即fun3()函数的then回调。fun4函数执行中断,所以此时:
    执行结果: 1,5,6,12,8,11
    宏任务队列:timer2 => timer7 => timer10
    微任务队列:thenFun3
    待执行函数:fun1(),fun2(),fun4()
  10. 第十步,继续顺序执行,后边是一个正儿八经的promise,那么自执行函数哈,执行它!所以我们打印15,然后resolve(),promise对象状态变为fulfilled,后边的then回调是异步的,需要去微任务队列中排队,所以咱们暂时给他取名then16哈。到此同步程序执行完毕,此时:
    执行结果: 1,5,6,12,8,11,15
    宏任务队列:timer2 => timer7 => timer10
    微任务队列:thenFun3 => then16
    待执行函数:fun1(),fun2(),fun4()
  11. 第十一步,因为同步代码执行完毕,调用栈空下来了,事件循环机制看到后,得赶紧给你分配任务呀!上边咱说过,JavaScript规定先执行微任务队列中的任务,所以先处理的是微任务thenFun3。即打印14,此时fun4()的调用彻底执行完毕。
    执行结果: 1,5,6,12,8,11,15,14
    宏任务队列:timer2 => timer7 => timer10
    微任务队列:then16
    待执行函数:fun1(),fun2()
  12. 第十二步,继续微任务队列,then16,也就是执行最下边的promise对象的then回调函数。里面就一句话console16,所以结果:
    执行结果: 1,5,6,12,8,11,15,14,16
    宏任务队列:timer2 => timer7 => timer10
    微任务队列:空
    待执行函数:fun1(),fun2()
  13. 第十三步,微任务队列清空,开始执行宏任务啦。第一个timer2,咱进入到timer2的定时器内部,顺序打印2。然后又是遇到一个promise,直接执行,打印3,promise对象执行完毕,让then去排队。所以此时的结果:
    执行结果: 1,5,6,12,8,11,15,14,16,2,3
    宏任务队列:timer7 => timer10
    微任务队列:then4
    待执行函数:fun1(),fun2()
  14. 第十五步,事件循环机制看到微任务队列中又有东西啦,于是还是先执行微任务队列中的then4的回调,所以console4被执行啦。此时timer2定时器彻底执行完毕。
    执行结果: 1,5,6,12,8,11,15,14,16,2,3,4
    宏任务队列:timer7 => timer10
    微任务队列:空
    运行中函数:fun1(),fun2()
  15. 第十六步,微任务队列又空啦,开始执行宏任务timer7,咱们进到这个定时器里面哈,顺序打印7,然后resolve,此时p2终于执行完成,状态也终于变成了fulfilled,执行结果固定为7。
    p1,p2都执行完成了。Promise.all对象的状态也终于变成了fulfilled,并将resolve的值[6,7]返回给了变量aa,这时它的then回调函数终于有资格去微任务队列中排队了。所以此时:
    执行结果: 1,5,6,12,8,11,15,14,16,2,3,4,7
    宏任务队列:timer10
    微任务队列:promiseAllThen
    待执行函数:fun1(),fun2()
  16. 第十六步,执行微任务promiseAllThen,即await后边的语句,await不用继续等待了,函数fun2可以继续执行,打印[6,7]和9,此时fun2函数彻底执行完毕。此时结果:
    执行结果: 1,5,6,12,8,11,15,14,16,2,3,4,7,[6,7],9
    宏任务队列:timer10
    微任务队列:空
    待执行函数:fun1()
  17. 第十七步,fun2执行完成,跳出fun2,返回到fun1中,同理,async函数返回的其实是个promise对象,所以await需要等待fun2函数的then回调,才能继续执行,所以让fun2函数返回的promise对象的then回调再去微任务队列中排队。
    执行结果: 1,5,6,12,8,11,15,14,16,2,3,4,7,[6,7],9
    宏任务队列:timer10
    微任务队列:fun2Then
    待执行函数:fun1()
  18. 执行微任务fun2Then,fun1中await终于等待结束,fun1函数也终于可以继续执行,打印13,fun1函数彻底执行完毕。所以此时:
    执行结果: 1,5,6,12,8,11,15,14,16,2,3,4,7,[6,7],9,13
    宏任务队列:timer10
    微任务队列:空
    待执行函数:无
  19. 第十八步,继续执行宏任务队列中的任务,timer10定时器中就一个打印10,此时,所有的任务队列都清空了,程序执行完毕,此时的结果:
    执行结果: 1,5,6,12,8,11,15,14,16,2,3,4,7,[6,7],9,13,10
    宏任务队列:空
    微任务队列:空
    待执行函数:无

此时,调用栈空了,宏任务队列空了,微任务队列也空了,所有函数调用完毕,执行结束!

当然了,以上分析全都是我的个人理解,有不对的地方,欢迎指教。

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值