ES6+ async/await
1. 前言
前面几节我们已经学习了 解决 异步的方案 Promise 和 Generator,上一节我们也通过 一个 案例使用 Promise + Generator 实现了 一个 比较好的异步 解决方 案。同时我们实现了 一个 简版的 co 库,让我们在使用 Generator 函数 处理异步任务时更加方便,但这不是 最完 美的 解决方 案。
本节我们将学习 ES7 推出的 async/await 其特性是对 JS 的异步编程进行了重要的改进,在不阻塞主线程的情况下,它给我们提供了使用同步 代码 的风格来编写异步任务的能力。另外,我们要明确的是 async/await 其实是 Promise + Generator 的语法糖,为了帮助我们像写同步 代码 一样书写异步 代码 , 代码 风格更优雅, 错误 捕获也更容易。
本节我们将通过对上一节案例的改造。在不需要 co 库的情况下直接使用 async/await 让我们更加深刻地理解异步方案的演变过程。
2. 改造上节案例
上一节 我们通过 一个 案例来讲解 Promise + Generator 在实际应用中的使用,通过 Generator 函数 和 yield 让异步 代码 看起来像同步 代码 一样执行。但是这样里面存在的 一个 问题就是 生成 器 函数 直接执行,需要手动处理。为了 解决 深层回调的问题我们借助了 co 库来帮助我们去执行 生成 器 函数 ,从而 解决 了回调地狱的问题。下面是上一节的 代码 。
const ajax = function ( api ) { return new Promise ( ( resolve , reject ) => { setTimeout ( ( ) => { if ( api === 'api_1' ) { resolve ( 'api_2' ) ; } if ( api === 'api_2' ) { resolve ( ) ; } } , ) } ) } function * getValue ( ) { const api = yield ajax ( 'api_1' ) ; const value = yield ajax ( api ) ; return value ; } co ( getValue ( ) ) . then ( res => { console . log ( res ) ; } )
上面的 代码 中 getValue 是 生成 器 函数 ,不能直接 调用 ,这里用 co 库来进行执行,然后通过 Promise 的链式 调用 获取 执行后的结果。但是这里借助了 co 的库,我们其实最希望的是能像执行普通 函数 一样直接 调用 getValue 就能执行并得到结果。 async/await 的出现就是为了抹平在 调用 时所做的额外步骤。那让我们看看 async/await 是怎么用的:
async function getValue ( ) { const api = await ajax ( 'api_1' ) ; const value = await ajax ( api ) ; console . log ( value ) return value ; } getValue ( ) // 控制台打印 value的值是:100
上面的 代码 中我们可以看出使用 async/await 定义的 getValue 函数 和 生成 器 函数 */yield 定义的基本相同,但是在执行时 async/await 定义的 函数 直接 调用 即可。从这里我们就能看到 async/await 的优点,无需过多的操作非常优雅和简洁。
3. 用法
上面我们基本了解了 async 函数 ,下面我们就来看看它的基本使用和需要注意的地方。
定义 一个 异步 函数 时需要使用 async 和 function 关键字一起来完成,类似 生成 器 函数 中的 yield 来暂停异步任务,在 async 函数 中使用 await 关键去等待异步任务返回的结果。
async 函数 其本质是 Promise + Generator 函数 组成的语法糖,它为了减少了 Promise 的链式 调用 ,解放了 Generator 函数 的单步执行。主要语法如下:
async function name ( [ p ara m [ , p ara m [ , ... p ara m ] ] ] ) { statements }
上面 代码 中的 statements 是 函数 主体的表达式,async 函数 可以通过 return 来返回 一个 值,这个返回值会被包装成 一个 Promise 实例,可以被链式 调用 。下面我们来看两段等价 代码 。
// 下面两段 代码 时相同的 async function foo ( ) { return } function foo ( ) { return Promise . resolve ( ) } // 下面两段 代码 时相同的 async function foo ( ) { await ; } function foo ( ) { return Promise . resolve ( ) . then ( ( ) => undefined ) }
上面的两段 代码 效果 时相同的,这里我们就不去探究 async 函数 是怎么实现的,其大概原理类似上节写的 co 库,有兴趣的小伙伴可以去 @L_ 301 _1@ 上去看看 async 函数 编译是什么样子的。
当在 async 函数 中返回的是 一个 普通值或 await 后跟 一个 普通值时,此时的 async 函数 是同步的。在 Promise 中失败是不能被 try...catch 捕获的,需要通过 catch 的方式来捕获 错误 。而使用 async 函数 则是可以通过 try...catch 来捕获。
async function foo ( ) { return new Error ( 'Throw an error' ) ; } foo ( ) . then ( res => { console . log ( res ) } ) . catch ( err => { console . error ( err ) // Error: Throw an error } ) async function foo2 ( ) { try { var v = await foo ( ) console . log ( v ) } catch ( e ) { console . log ( e ) ; // Error: Throw an error } } foo2 ( )
上面的 代码 中在执行 foo() 直接抛出了 一个 错误 ,而 Promise 和 async/await 对 错误 的捕获是不同的,我们知道 Promise 是通过 then 中的失败回调和 catch 来捕获 错误 的,而 async 函数 使用的是 try...catch 更像同步的方式。
3.1 错误 捕获
但是有个问题,当程序需要同时处理多个异步任务时,那我们使用 async/await 怎样捕获那个异步任务出现 错误 呢?try 块中的 代码 只要程序出现 错误 就会抛出 错误 ,但是不知道是哪个异步任务出错了不利于定位问题。如果使用多个 try...catch :
const task = function ( num ) { return new Promise ( ( resolve , reject ) => { setTimeout ( ( ) => { if ( num === ) { reject ( 'throw error' ) } else { resolve ( 'imooc' ) ; } } , ) } ) } async function foo ( ) { try { let res1 = await task ( ) ; try { let res2 = await task ( ) ; try { let res3 = await task ( ) ; } catch ( e ) { console . log ( 'res3' , e ) } } catch ( e ) { console . log ( 'res2' , e ) } } catch ( e ) { console . log ( 'res1' , e ) } } foo ( ) // res3 throw error
看到上面的 代码 你是不是觉得很难受啊,又回到了嵌套地狱的原始问题了。async 函数 在异常捕获时,没有非常完美的 解决方 案,这主要源自依赖 try...catch 对 错误 的捕获。但有一些还算比较优雅的 解决方 案,我们已经知道了 async 函数 返回的是 一个 Promise 那么我们是不是可以使用 Promise 的 catch 来捕获呢?答案是当然的呢。
async function foo ( ) { let res1 = await task ( ) . catch ( err => console . log ( 'res1' , err ) ) ; let res2 = await task ( ) . catch ( err => console . log ( 'res2' , err ) ) ; let res3 = await task ( ) . catch ( err => console . log ( 'res3' , err ) ) ; } foo ( ) // res3 throw error
上面的 代码 看起来就比嵌套的 try...catch 感觉好很多,这也是 一个 比较好的 解决方 式。在使用 catch 时需要弄清楚 Promise 和 async 函数 之 间的 关系,不然就很难理解这种写法。
3.2 滥用 async/await
既然 async/await 这么优雅简洁,那在编程的过程中都使用这个就好啦!其实这里是 一个 坑,很多时候 async/await 都会被滥用导致程序卡顿,执行时间过长。
async function foo ( ) { let res1 = await task ( ) ; let res2 = await task ( ) ; let res3 = await task ( ) ; return { res1 , res2 , res3 } } foo ( )
在很多时候我们会写成这样的 代码 ,如果后 一个 任务依赖前 一个 任务这样写完全没问题,但是如果是三个独立的异步任务,那这样写就会导致程序执行时间加长。这样的 代码 过于同步化,我们需要牢记的是 await 看起来是同步的,但它仍然属于异步的 内容 ,最终还是走的回调,只是语言底层给我们做了很多工作。
针对没有关联的异步任务我们需要把它们解开,
const task = function ( num ) { return new Promise ( ( resolve , reject ) => { setTimeout ( ( ) => { resolve ( 'imooc ' + num ) ; } , ) } ) } async function foo ( ) { let res1Promes = task ( ) ; let res2Promes = task ( ) ; let res3Promes = task ( ) ; let res1 = await res1Promes ; let res2 = await res2Promes ; let res3 = await res3Promes ; console . log ( { res1 , res2 , res3 } ) return { res1 , res2 , res3 } } foo ( ) ; // { res1: 'imooc 100', res2: 'imooc 200', res3: 'imooc 300' }
这里需要明白的一点是:为什么要把 task 拿到 await 外面去执行呢?await 的本质就是暂停异步任务,等待返回结果,等到结果返回后就会继续往下执行。还要知道的是每个 task 都是 一个 异步任务,像之前的那种写法,await 会等待上 一个 异步任务完成才会走下 一个 。而我们把 task 拿出来了,也就是每个 task 会按照异步的方式去执行。这个时候三个 task 都已经开始执行了,当遇到 await 就只需要等到任务完成就行。所需要的时间是异步任务中耗时最长的,而不是之前的总和。
4. 小结
本节我们主要通过延续上一节的案例,用 async 函数 给出了最优的 解决方 案,从而完善了整个异步演变的过程,让我们更加清晰地理解为什么会有 Promise?为什么会有 生成 器?为什么会有 async/await?由浅入深层层递进地讲解了 ES6 以后对异步任务处理的演变。然后我们主要学习了 async 函数 的基本使用和 错误 处理的捕获。最后,我们讲解了如果不滥用 async 函数 的案例,让我们在以后写程序的过程中更加得心应手。
查看更多关于ES6+ async/await的详细内容...