先来一碗毒鸡汤润润喉:
经常失恋的人总是会说:没关系,我相信肯定会有一个懂我的人一直在等着我!
阎王说:吼吼,没想到是我吧!
不求甚解之 ---- 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 - 第二步,碰到setTimeout,尽管它的延时时间为0哈,也给我乖乖到宏任务队列中排队去!咱给它起个名字,省的弄混了,就叫timer2吧,因为第一句是打印2,到时候咱们好找。所以到此:
执行结果: 1
宏任务队列:timer2 - 第三步,打印5,没毛病,到此结果:
执行结果: 1,5
宏任务队列:timer2 - 第四步,一个promise对象,大家知道promise是个自执行函数哈,所以什么都不用想,执行它,打印6。此时的执行结果:1,5,6。
然后碰到resolve了,resolve固定promise对象的执行结果,所以到此,p1执行完成,resolve值为6,状态变为fulfilled。
执行结果: 1,5,6
宏任务队列:timer2 - 第五步,又是一个promise对象哈,又是一个自执行函数,还是直接执行它!但是不同的是里面是个定时器,虽然是包含在自执行函数中的定时器,那它也是个定时器呀,所以,去排队,咱给这个定时器起名,叫timer7哈(不要纠结名字,咱们只是为了便于查找)。所以这个promise对象状态一直为pending。
执行结果: 1,5,6
宏任务队列:timer2 => timer7 - 第六步,继续往下看,定义了三个函数,只是定义,没有调用,所以不执行,咱就不管它。
- 第七步,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() - 第八步,继续顺序执行哈,调用fun4(),咱们跳到fun4函数体里面,发现第一句话是await fun3(),所以咱再进到fun3函数体里面去,发现里面是两句话,显示一个定时器,然后是一个console。所以定时器呢去排队,console11正常打印。那么此时:
执行结果: 1,5,6,12,8,11
宏任务队列:timer2 => timer7 => timer10
待执行函数:fun1(),fun2(),fun3(),fun4() - 第九步,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() - 第十步,继续顺序执行,后边是一个正儿八经的promise,那么自执行函数哈,执行它!所以我们打印15,然后resolve(),promise对象状态变为fulfilled,后边的then回调是异步的,需要去微任务队列中排队,所以咱们暂时给他取名then16哈。到此同步程序执行完毕,此时:
执行结果: 1,5,6,12,8,11,15
宏任务队列:timer2 => timer7 => timer10
微任务队列:thenFun3 => then16
待执行函数:fun1(),fun2(),fun4() - 第十一步,因为同步代码执行完毕,调用栈空下来了,事件循环机制看到后,得赶紧给你分配任务呀!上边咱说过,JavaScript规定先执行微任务队列中的任务,所以先处理的是微任务thenFun3。即打印14,此时fun4()的调用彻底执行完毕。
执行结果: 1,5,6,12,8,11,15,14
宏任务队列:timer2 => timer7 => timer10
微任务队列:then16
待执行函数:fun1(),fun2() - 第十二步,继续微任务队列,then16,也就是执行最下边的promise对象的then回调函数。里面就一句话console16,所以结果:
执行结果: 1,5,6,12,8,11,15,14,16
宏任务队列:timer2 => timer7 => timer10
微任务队列:空
待执行函数:fun1(),fun2() - 第十三步,微任务队列清空,开始执行宏任务啦。第一个timer2,咱进入到timer2的定时器内部,顺序打印2。然后又是遇到一个promise,直接执行,打印3,promise对象执行完毕,让then去排队。所以此时的结果:
执行结果: 1,5,6,12,8,11,15,14,16,2,3
宏任务队列:timer7 => timer10
微任务队列:then4
待执行函数:fun1(),fun2() - 第十五步,事件循环机制看到微任务队列中又有东西啦,于是还是先执行微任务队列中的then4的回调,所以console4被执行啦。此时timer2定时器彻底执行完毕。
执行结果: 1,5,6,12,8,11,15,14,16,2,3,4
宏任务队列:timer7 => timer10
微任务队列:空
运行中函数:fun1(),fun2() - 第十六步,微任务队列又空啦,开始执行宏任务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() - 第十六步,执行微任务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() - 第十七步,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() - 执行微任务fun2Then,fun1中await终于等待结束,fun1函数也终于可以继续执行,打印13,fun1函数彻底执行完毕。所以此时:
执行结果: 1,5,6,12,8,11,15,14,16,2,3,4,7,[6,7],9,13
宏任务队列:timer10
微任务队列:空
待执行函数:无 - 第十八步,继续执行宏任务队列中的任务,timer10定时器中就一个打印10,此时,所有的任务队列都清空了,程序执行完毕,此时的结果:
执行结果: 1,5,6,12,8,11,15,14,16,2,3,4,7,[6,7],9,13,10
宏任务队列:空
微任务队列:空
待执行函数:无
此时,调用栈空了,宏任务队列空了,微任务队列也空了,所有函数调用完毕,执行结束!
当然了,以上分析全都是我的个人理解,有不对的地方,欢迎指教。