什么是async?什么是await?
在JavaScript
的世界,同步sync
异步async
的爱恨情仇,就如同偶像剧一般的剪不断理还乱,特别像是setTimeout
、setInterval
、MLHttpRequest
或fetch
这些同步、异步混杂的用法,都会让人一个头两个大,幸好ES6
出现了promise
,ES7
出现了async、await
,帮助我们可以更容易的进行代码逻辑的撰写。
对于同步和异步理解,使我觉得比较好理解的方法:「同一个跑道vs不同跑道」,透过跑步的方式,就更容易明白同步和异步。
- 同步:在「同一个跑道」比赛「接力赛跑」,当棒子没有交给我,我就得等你,不能跑。
- 异步:在「不(非)同跑道」比赛「赛跑」,谁都不等谁,只要轮到我跑,我就开始跑。
在ES7
里async
的本质是promise
的语法糖,只要function
标记为async
,就表示里头可以撰写await
的同步语法,而await
顾名思义就是「等待」,它会确保一个promise
函数都执行到解决(resolve
)或出错( reject
)后才会进行下一步,当async function
的内容全都结束后,会返回一个promise
,这表示后方可以使用.then
语法来做连接,基本的代码就像下面这样:
async function a(){
await b();
..... // 等 b() 完成後才會執行
await c();
..... // 等 c() 完成後才會執行
await new Promise(resolve=>{
.....
});
..... // 上方的 promise 完成後才會執行
}
a();
a().then(()=>{
..... // 等 a() 完成後接著執行
});
利用async 和await 做个「漂亮的等待」
比较了解async
和await
的意思之后,就来试试看做个「漂亮的等待」,使用ES6
的promise
来实现delay
(如同下方的代码范例) ,这个delay
透过.then
来完成一步一步的串接,虽然逻辑上很清楚,但若要实作比较复杂的流程,就得把每个代码写在对应的callback
里,也就没有想像的容易,这就是「不太漂亮的等待」 (使用setTimeout的做法就是不漂亮的等待)。
const delay = (s) => {
return new Promise(resolve => {
setTimeout(resolve,s);
});
};
delay().then(() => {
console.log(1); // 顯示 1
return delay(1000); // 延遲ㄧ秒
}).then(() => {
console.log(2); // 顯示 2
return delay(2000); // 延遲二秒
}).then(() => {
console.log(3); // 顯示 3
});
如果我们把上面的代码修改为async
和await
的写法,突然就发现代码看起来非常的干净,因为await
会等待收到resolve
之后才会进行后面的动作,如果没有收到就会一直处在等待的状态,所以什么时候该等待,什么时候该做下一步,就会非常清楚明了,这也就是我所谓「漂亮的等待」。
注意,
await
一定得运行在async function
内!
~async function{ // ~ 開頭表示直接執行這個 function,結尾有 ()
const delay = (s) => {
return new Promise(function(resolve){ // 回傳一個 promise
setTimeout(resolve,s); // 等待多少秒之後 resolve()
});
};
console.log(1); // 顯示 1
await delay(1000); // 延遲ㄧ秒
console.log(2); // 顯示 2
await delay(2000); // 延遲二秒
console.log(3); // 顯示 3
}();
搭配Promise
基本上只要有async
和await
的地方,就一定有promise
的存在,promise
顾名思义就是「保证执行之后才会做什么事情」,刚刚使用了async
、await
和promise
改善setTimeout
这个容易出错的异步等待,针对setInterval
,也能用同样的做法修改
举例来说,下面的代码执行之后,并「不会」如我们预期的「先显示1
,再显示haha0...haha5
,最后再显示2
」,而是「先显示1
和2
,然后再出现haha0...haha5
」,因为虽然代码逻辑是从上往下,但在count function
里头是异步的语法,导致自己走自己的路,也造成了结果的不如预期。
const count = (t,s) => {
let a = 0;
let timer = setInterval(() => {
console.log(`${t}${a}`);
a = a + 1;
if(a>5){
clearInterval(timer);
}
},s);
};
console.log(1);
count('haha', 100);
console.log(2);
这时我们可以透过async、await
和promise
进行修正,在显示1
之后,会「等待」count function
结束后再显示2
。
~async function(){
const count = (t,s) => {
return new Promise(resolve => {
let a = 0;
let timer = setInterval(() => {
console.log(`${t}${a}`);
a = a + 1;
if(a>5){
clearInterval(timer);
resolve(); // 表示完成
}
},s);
});
};
console.log(1);
await count('haha', 100);
console.log(2);
}();
链式调用
假设一个业务,分多个步骤完成,每个步骤都是异步的,而且依赖于上一个步骤的结果。我们仍然用
setTimeout
来模拟异步操作:
/**
* 传入参数 n,表示这个函数执行的时间(毫秒)
* 执行的结果是 n + 100,这个值将用于下一步骤
*/
function takeLongTime(n) {
return new Promise(resolve => {
setTimeout(() => resolve(n + 100), n);
});
}
function step1(n) {
console.log(`step1 with ${n}`);
return takeLongTime(n);
}
function step2(n) {
console.log(`step2 with ${n}`);
return takeLongTime(n);
}
function step3(n) {
console.log(`step3 with ${n}`);
return takeLongTime(n);
}
Promise
实现
function init() {
console.time("start init");
const time1 = 100;
step1(time1)
.then(time2 => step2(time2))
.then(time3 => step3(time3))
.then(result => {
console.log(`result is ${result}`);
console.timeEnd("end init");
});
}
init();
// step1 with 100
// step2 with 200
// step3 with 300
// result is 400
async awit
实现
async function init() {
console.time("start init");
const time1 = 100;
const time2 = await step1(time1);
const time3 = await step2(time2);
const result = await step3(time3);
console.log(`result is ${result}`);
console.timeEnd("end init");
}
init();
结果和之前的 Promise
实现是一样的,但是这个代码看起来是不是清晰得多
关于文中几个知识点概念
setTimeout()
方法用于在指定的毫秒数后调用函数或计算表达式。
function fn1(){
console.log("你好");
}
setTimeout(fn1,3000);
setInterval()
每隔一段时间
执行一次指定的语句或函数,是个重复性的操作。
setInterval(fn1,8000);
小结
坦白说只要你一但熟悉了async
和await
,就真的回不去了,虽然说callback
仍然是代码开发里必备的功能,但对于同步和异步之间的转换,以后就交给async
和await
来处理吧!