async 函数本质上近似于 Generator 函数 + 自动执行器的语法糖,由 ES2017 标准引入,并且在 nodejs v8.0 及其以上版本中提供了原生支持,下面我们介绍一下 async 函数的用法和特点
async 的特点 首先我们举一个 async 的例子方便下面的说明
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 const fs = require ('fs' );let readFile = function (fileName ) { return new Promise (function (resolve, reject ) { fs.readFile(fileName, function (err, data ) { if (err) reject(err); resolve(data); }) }); }; let asyncReadFile = async function ( ) { let f1 = await readFile('./txt/A.txt' ); let f2 = await readFile('./txt/B.txt' ); let f3 = await readFile('./txt/C.txt' ); console .log(`${f1} ${f2} ${f3} ` ); }; asyncReadFile();
上面的例子中,我们使用 async 函数依次读取了文件 A,B,C 并输出了最终结果
Async 与 Generator 之间的异同
async 函数内置执行器,不需要像 Generator 函数那般使用类似 co 这样的外部执行器
更好的语义
更广的适用性,co 模块后面只能跟 Promise 对象,await 后面除了可以跟 Promise 对象,也可以跟原始类型值(number、String、Boolean)此时等同于同步操作
async 函数的返回值是一个 Promise 对象
Async 函数的另一个例子 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 function timeout (ms ) { return new Promise (resolve => setTimeout(resolve, ms)); } async function Print (value, ms ) { await timeout(ms); console .log(value) } Print('Hello' , 500 ); async function timeout (ms ) { await new Promise (resolve => setTimeout(resolve, ms)); } async function Print (value, ms ) { await timeout(ms); console .log(value) } Print('Hello' , 500 );
上面的代码将会在 500ms 后输出 ‘Hello’
async 内部 return 的值会成为 then 方法回调函数的参数 1 2 3 4 5 6 async function fn ( ) { return 'Hello World' ; } fn().then(v => console .log(v));
上面的例子中,async 函数内部 return 语句返回的值,会成为 then 回调函数的参数。
async 函数内部抛出的错误会导致返回的 Promise 对象变成 Rejected 状态 1 2 3 4 5 6 7 8 9 10 11 12 13 async function fn ( ) { return x / 2 ; } fn().then( v => console .log(v), e => console .log(e) ); fn().then( v => console .log(v) ).catch(e => console .log(e));
上面定义的函数 fn 中,x 未定义,故而会抛出异常,导致返回的 Promise 对象被 rejected 掉。该异常可以在 reject 回调中处理,也可以在 Promise 对象的 catch 语句中处理
async 函数返回的 Promise 对象只有在所有 await 函数都执行完毕后,再执行 then 方法 1 2 3 4 5 6 7 8 9 10 11 12 async function fn ( ) { let f1 = await new Promise (resolve => setTimeout(resolve, 100 , 'Hello' )); let f2 = await new Promise (resolve => setTimeout(resolve, 200 , 'World' )); let f3 = await new Promise (resolve => setTimeout(resolve, 300 , '!' )); return `${f1} ${f2} ${f3} ` } fn().then( v => console .log(v), e => console .log(e) );
上面的例子中,有三个 await 命令,后面跟着分别执行时长为 100ms 200ms 300ms 的三个 Promise 对象,async 函数会等这三个 await 命令后面跟着的操作都执行完成后,在执行 then 方法,故而我们可以发现,最后 then 方法输出的是所有操作的返回值的总和
await 命令后面跟的若不是一个 Promise 对象,则会自动将之转换成 Promise 对象 1 2 3 4 5 6 async function fn ( ) { return await 123 ; } fn().then(v => console .log(v));
只要有一个 await 命令后面的 Promise 被 reject 掉,则整个 async 函数将终止 1 2 3 4 5 6 7 8 9 10 11 async function fn ( ) { await Promise .reject('出错了' ); await Promise .resolve('Hello World !' ); } fn().then( v => console .log(v), e => console .log(e) );
上面的例子中,由于第一句 await 语句即被 reject 掉了,故而后续的 await 语句不再执行,该 async 函数终止
await reject 后不终止 async 函数 若是我们希望即使 async 函数中即使有一个异步被 reject 掉也不影响其他异步的执行,则可以使用以下两种方式去处理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 async function fn ( ) { try { await new Promise ((resolve, reject ) => setTimeout(reject, 50 , 'error 1' )) } catch (e) { console .log(e); } return await new Promise (resolve => setTimeout(resolve, 50 , 'Hello !' )) } fn().then( v => console .log(v), ); async function fn ( ) { await new Promise ((resolve, reject ) => setTimeout(reject, 50 , 'error 1' )).catch(e => console .log(e)); return await new Promise (resolve => setTimeout(resolve, 50 , 'Hello !' )) } fn().then( v => console .log(v), );
async 和普通函数的一个异同 1 2 3 4 5 6 7 8 9 10 function fn ( ) { try { new Promise ((resolve, reject ) => setTimeout(reject, 50 , 'error 1' )) } catch (e) { console .log(e); } } fn();
和普通函数相比,普通函数的 try catch 无法捕获异步操作抛出的错误,但是 async 中的可以
若是 await 后跟的异步操作报错,则返回的 Promise 也会被 reject 掉 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 async function fn ( ) { await new Promise ((resolve, reject ) => { throw new Error ('error !' ); }) } fn().then( v => console .log(v), e => console .log(e) ); fn().then( v => console .log(v) ).catch(e => console .log(e));
上面的代码中,async 函数执行后,await 后面的异步操作产生异常,同样会导致该返回的 Promise 对象的状态变为 reject。可以被 catch 或是 reject 回调捕捉到
当然同样的,也可以在 try catch 中捕获该处理该种异常
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 async function fn ( ) { try { await new Promise (() => { throw new Error ('error' ) }) } catch (e) { console .log(e) } return await 'Hello' } fn().then(v => console .log(v));
使用 try catch 进行多次异步请求 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 const fetch = require ('node-fetch' );const max_repeat = 3 ;async function test ( ) { let i = 0 ; for (i = 0 ; i < max_repeat; i++) { try { await fetch('http://www.baidu.com' ); break ; } catch (e) { console .log(e) } } console .log(i); } test();
上面的例子中,如果 await 操作成功,则会使用 break 终止循环;如果失败,则会被 catch 语句捕捉,然后进入下一轮循环
async 使用注意点
建议把 async 函数的 await 命令用 try catch 扩起来,捕获异常
若是异步操作之间没有先后顺序,建议同时触发
async 顺序 & 并发执行异步操作 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 function f1 ( ) { return new Promise (resolve => setTimeout(resolve, 100 , 'Hello' )); } function f2 ( ) { return new Promise (resolve => setTimeout(resolve, 200 , 'World' )); } function f3 ( ) { return new Promise (resolve => setTimeout(resolve, 300 , '!' )); } async function fn1 ( ) { let start = new Date (); let v1 = await f1(); let v2 = await f2(); let v3 = await f3(); let end = new Date (); console .log(`fn1 输出:${v1} ${v2} ${v3} , 耗时:${end - start} ` ); } async function fn2 ( ) { let start = new Date (); let [v1, v2, v3] = await Promise .all([f1(), f2(), f3()]); let end = new Date (); console .log(`fn2 输出:${v1} ${v2} ${v3} , 耗时:${end - start} ` ); } fn1(); fn2();
从上面的例子中可以看出,顺序写法中,程序会等待上一个异步完成后再去执行下一个异步,所以总耗时为 607ms,正好是上面 f1, f2, f3 三个异步任务的耗时总和 (100ms + 200ms + 300ms = 700ms)。其中多出来的 7ms 是函数调用的时间开销。
使用 async 顺序 & 并发调用 (结合 Promise.all) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 const fetch = require ('node-fetch' );let books = [ 'https://api.douban.com/v2/book/1220560' , 'https://api.douban.com/v2/book/1220561' , 'https://api.douban.com/v2/book/1220562' ]; async function sequence ( ) { for (let book of books) { let response = await fetch(book).then(v => v.json()); console .log(response) } } sequence(); async function asyncFn ( ) { let [v1, v2, v3] = await Promise .all(books.map(url => fetch(url).then(v => v.json()))); console .log(v1, v2, v3); } asyncFn();
可以使用上述两种方式,进行继发(按顺序)的异步调用,或是并行的异步调用。
需要注意的是 Promise 对象具有定义立即执行的特点,所以使用 Promise.all 方法的时候,需要注意使用的时机,最好在使用时才定义!
使用 async 顺序 & 并发调用 上面的 Promise.all 虽然可以做到并发执行,但是 Promise.all 有一个特性,当其中某一个 promise 函数被 reject 掉后,整个 Promise.all 会被 reject 掉,且以第一个被 Rejected 的实例的返回值作为整个 Promise.all 的返回值。
从上面 Promise.all 的特性可以看出,只有当所有的 fetch 操作成功才能保证 Promise.all 的返回值是我们真正想要的。要是有一个 fetch 操作失败,则所有的 fetch 操作变得没有意义,这显然是无法接受的,下面介绍另一种方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 const fetch = require ('node-fetch' );let books = [ 'https://api.douban.com/v2/book/1220560' , 'https://api.douban.com/v2/book/1220561' , 'https://api.douban.com/v2/book/1220562' ]; async function asyncFn ( ) { let results = books.map(async url => { try { let response = await fetch(url ); return response.json(); } catch (e) { return 'NONE' ; } }); for (let item of results) { let data = await item; console .log(data); } } asyncFn();
上面的例子中,我们使用 map 函数来对所有的 api 进行并发 fetch 操作。再用 for of 操作,进行读出 fetch 结果,for of 中使用 await 是因为在 map 函数中使用了 async 函数,async 函数返回的是一个 promise 对象,故而在 for of 循环读出数据的时候使用 await 使其变成正常 json 数据