JS中的async和await
前言
异步编程允许我们在执行一个长时间任务时,程序不需要进行等待,而是继续执行之后的代码,直到这些任务完成之后再回来通知你。早期的异步编程是通过回调函数实现的,这种编程的模式避免了程序的阻塞,大大提高了CPU的执行效率,尤其适用于一些前后端数据库交互的操作。然而回调函数会出现回调地狱的情况,为了解决这一问题,ES6 出现了 Promise。那为何 ES7 又会出现 async/await 呢?它对比 Promise 又有什么优势呢?那么接下来就让我们一起来看看吧!
什么是async/await?
async/await 是编写异步代码的新方式,是基于 Promise 实现的。
async/await 使得异步代码看起来像同步代码,这正是它的魔力所在。
async
我们可以使用 async 来声明一个函数为异步函数,如下:
async function fn1(){ return 88; } async function fn2(){ return Promise.resolve(66); } console.log('result1:',fn1()); console.log('result2:',fn2());
从打印的结果中我们可以看出执行一个 async 函数,返回的都是 Promise 对象。如果返回的是像函数 fn1 中那样普通的值的话,它会帮你封装成一个 Promise 对象。
await 在等什么?
现在我们已经知道 async 是来声明一个函数为异步函数的,那么 await 又是干什么的呢?我们继续先上一段代码看看:
async function fn3(){ const p3 = Promise.resolve(66); p3.then(data => { console.log('data of then:',data); }) const data = await p3; console.log('data of await:',data); } fn3();
看了打印结果后,我们知道了,await等的是结果,promise.then 成功的情况对应 await。也就是 await 可以获取 Promise 函数中 resolve 或 reject 的值。注意 await 关键字只能放在 async 函数内部。如果用在普通函数就会报错。
async/await 的执行顺序
可能大家都知道 await 会让出线程,阻塞后面的代码,那么下面例子中, 'async2' 和 'script start' 谁先打印呢?
是从左向右执行,一旦碰到await直接跳出, 阻塞async2()的执行?
还是从右向左,先执行async2后,发现有await关键字,于是让出线程,阻塞代码呢?
async function async1() { console.log( 'async1 start' ) await async2() console.log( 'async1 end' ) } async function async2() { console.log( 'async2' ) } async1() console.log( 'script start' )
从打印结果可以看出,从右向左的。先打印async2,后打印的script start
之所以提一嘴,是因为我经常看到这样的说法,「一旦遇到await就立刻让出线程,阻塞后面的代码」
这样的说法,会让我误以为,await后面那个函数, async2()也直接被阻塞呢。
await 等到之后,做了一件什么事情?
那么右侧表达式的结果,就是await要等的东西。
等到之后,对于await来说,分2个情况
- 不是promise对象
- 是promise对象
如果不是 promise , await会阻塞后面的代码,先执行async外面的同步代码,同步代码执行完,再回到async内部,把这个非promise的东西,作为 await 表达式的结果
如果它等到的是一个 promise 对象,await 也会暂停 async 后面的代码,先执行async外面的同步代码,等着 Promise 对象 fulfilled,然后把 resolve 的参数作为 await 表达式的运算结果。
try...catch 捕获异常
捕获异常是使用 try...catch 的方式来处理,因为 await 后面跟着的是 Promise 对象,当有异常的情况下会被 Promise 对象的内部 catch 捕获,而 await 就是一个 then 的语法糖,并不会捕获异常, 那就需要使用 try...catch 来捕获异常,并进行相应的逻辑处理。
async function fn5(){ const p5 = Promise.reject('捕获异常'); try{ const data5 = await p5; console.log('data5:',data5); }catch (e){ console.log('e',e); } } fn5()
async/await 和 Promise 对比
用 setTimeout 模拟耗时的异步操作,先来看看 Promise 怎么写:
function takeLongTime() { return new Promise(resolve => { setTimeout(() => resolve("long_time_value"), 1000); }); } takeLongTime().then(v => { console.log("got", v); });
改用 async/await 写:
function takeLongTime() { return new Promise(resolve => { setTimeout(() => resolve("long_time_value"), 1000); }); } async function test(){ const a = await takeLongTime(); console.log(a); } test();
一个疑问产生了,这两段代码,两种方式对异步调用的处理(实际就是对 Promise 对象的处理)差别并不明显,甚至使用 async/await 还需要多写一些代码,那它的优势到底在哪?
async/await 的优势在于处理 then 链
单一的 Promise 链并不能发现 async/await 的优势,但是,如果需要处理由多个 Promise 组成的 then 链的时候,优势就能体现出来了(很有意思,Promise 通过 then 链来解决多层回调的问题,现在又用 async/await 来进一步优化它)。
假设一个业务,分多个步骤完成,每个步骤都是异步的,而且依赖于上一个步骤的结果。我们仍然用 setTimeout
来模拟异步操作:
/** * 传入参数 n,表示这个函数执行的时间(毫秒) * 执行的结果是 n + 200,这个值将用于下一步骤 */ function takeLongTime(n) { return new Promise(resolve => { setTimeout(() => resolve(n + 200), 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 doIt() { console.time("doIt"); const time1 = 300; step1(time1) .then(time2 => step2(time2)) .then(time3 => step3(time3)) .then(result => { console.log(`result is ${result}`); console.timeEnd("doIt"); }); } doIt(); // c:\var\test>node --harmony_async_await . // step1 with 300 // step2 with 500 // step3 with 700 // result is 900 // doIt: 1507.251ms
如果用 async/await 来实现呢,会是这样:
async function doIt() { console.time("doIt"); const time1 = 300; const time2 = await step1(time1); const time3 = await step2(time2); const result = await step3(time3); console.log(`result is ${result}`); console.timeEnd("doIt"); } doIt();
参考资料:https://segmentfault.com/a/1190000007535316
https://www.cnblogs.com/fundebug/p/10095355.html