1.含义
async函数简洁点说就是Generator函数的语法糖。
示例:一个读取文件的异步操作,逐步执行,使用Generator函数执行
1 const fs = require('fs') 2 3 const readFile = function (fileName) { 4 return new Promise(function (resolve, reject) { 5 fs.readFile(fileName, function (error, data) { 6 if (error) return reject(error); 7 resolve(data); 8 }); 9 }) 10 } 11 12 const gen = function* (){ 13 const f1 = yield readFile('./a') 14 const f2 = yield readFile('./b') 15 console.log("文件读取结束"); 16 }
使用async函数执行:
1 const gen = async function (){ 2 const f1 = await readFile('./a') 3 const f2 = await readFile('./b') 4 console.log("文件读取结束"); 5 }
可以看到,async函数就是将Generate函数的星号(*)替换成async,将yield替换为await而已。
async函数对Generator函数的改进体现在以下四点:
- 内置执行器:
async
函数的执行,与普通函数一模一样,只要一行-
asyncReadFile();
上面代码调用了asyncReadFile函数,然后就会自动执行,输出最后结果。
-
- 更好的语义:
async
表示函数里有异步操作,await
表示紧跟在后面的表达式需要等待结果。
- 更广的适用性:
- Generator 函数的执行必须靠执行器,所以才有了
co
模块。 co
模块约定,yield
命令后面只能是 Thunk 函数或 Promise 对象,而async
函数的await
命令后面,可以是 Promise 对象和原始类型的值(数值、字符串和布尔值,但这时会自动转成立即 resolved 的 Promise 对象)
- Generator 函数的执行必须靠执行器,所以才有了
- 返回值是Promise
- async函数的返回值是Promise对象(Generator函数返回值是Iterator对象)。可以使用then方法指定下一步操作。
2.基本用法
async函数返回的是一个Promise对象,可以使用then方法添加回调函数。当函数执行的时候,一旦遇到await就会先返回,等到异步操作完成,再接着执行函数体后面的语句:
1 function p1(val) { 2 return new Promise((resolve, reject) => { 3 setTimeout(() => { 4 resolve("p1传递数据:" + val) 5 }, 3000) 6 }) 7 } 8 9 function p2(val) { 10 return new Promise((resolve, reject) => { 11 setTimeout(() => { 12 resolve("p2传递数据:" + val) 13 }, 2000) 14 }) 15 } 16 17 async function asyncTest(value) { 18 const vp1 = await p1(value) 19 const vp2 = await p2(vp1) 20 return vp2 21 } 22 23 asyncTest("张三").then(res => { 24 console.log(res) // p2传递数据:p1传递数据:张三 25 })
上面代码中,p1,p2是两个异步操作。在asyncTest函数中现后调用了p1和p2,p2接收了p1的返回值作为参数。在5s以后输出了相应的值。
async函数的使用形式:
- 函数声明式:
-
async function foo() {}
-
- 函数表达式:
-
const foo = async function () {}
-
- 对象的方法:
-
let obj = { async foo() {} } obj.foo().then(...)
-
- class的方法:
-
class Storage { constructor() { this.cachePromise = caches.open('avatars'); } async getAvatar(name) { const cache = await this.cachePromise; return cache.match(`/avatars/${name}.jpg`); } } const storage = new Storage(); storage.getAvatar('jake').then(…)
-
- 箭头函数:
-
const foo = async () => {}
-
3.语法
async函数的语法规则总体上比较见到那,难点是错误处理机制。
(1)返回Promise对象
- async函数内部return语句返回的值,会成为then方法回调函数的参数
-
async function f() { return 'hello world'; } f().then(v => console.log(v)) // "hello world"
-
async
函数内部抛出错误,会导致返回的 Promise 对象变为reject
状态。抛出的错误对象会被catch
方法回调函数接收到。-
async function f() { throw new Error('出错了'); } f().then( v => console.log('resolve', v), e => console.log('reject', e) ) //reject Error: 出错了
-
(2)Promise对象状态的变化
async函数返回的Promise对象,必须等到内部所有的await命令后面的Promise对象执行完,才会发生状态改变,除非遇到return语句或者抛出错误。也就是说,只有async函数内部的异步操作执行完,才会执行then方法指定的回调函数。
(3)await命令
正常情况下,await命令后面是一个Promise对象,返回该对象的姐u共。如果不是Promise对象,就会直接返回对应的值
async function f() { // 等同于 // return 123; return await 123; } f().then(v => console.log(v)) // 123
另一种情况是,await
命令后面是一个thenable
对象(即定义了then
方法的对象),那么await
会将其等同于 Promise 对象。
class Sleep { constructor(timeout){ this.timeout = timeout } then(resolve,reject){ const startTime = Date.now(); setTimeout(()=>{ resolve(Date.now() - startTime) },this.timeout) } } (async ()=>{ const sleepTime = await new Sleep(1000); console.log(sleepTime); })() // 1000
上面代码中,await命令后面是一个Sleep对象的实例。这个实例不是Promise对象,但是因为定义了then方法,await会将其视为Promise处理。
比如实现一个简单的程序休眠
function sleep(interval){ return new Promise((resolve,reject)=>{ setTimeout(resolve,interval) }) } async function one2Async(){ for(let i = 1;i<=5;i++){ console.log(i); await sleep(1000) } } one2Async();
上面代码将会每隔1s输出一个i。不过需要注意的是,这里的await后面的promise对象仍然属于微任务队列的内容,如果在调用了one2Async方法后面继续添加语句,这语句仍然属于主线程(调用栈),会优先执行。如果需要在所有的都执行结束再执行期望的语句,还是需要放在then方法中执行:
one2Async().then(() => { console.log("1111"); })
await命令后面的Promise对象如果变成reject状态,则reject的参数会被catch方法的回调函数接收到。
async function f() { await Promise.reject('出错了'); } f() .then(v => console.log("vvv:", v)) .catch(e => console.log("eee:", e)) // eee:出错了
注意,上面代码中,await语句前面没有return,但是reject方法的参数依然传入了catch方法的回调函数。这里如果在await前面加上return,效果是一样的。
任何一个await语句后面的Promise对象变成reject状态,那么整个async函数都会中断执行。
async function f() { await Promise.reject('出错了'); await Promise.resolve('hello world'); // 不会执行 }
但是有时候,我们希望即使前一个异步操作失败,呀不要中断后面的异步操作。这时可以将第一个await放在try...catch结构里面。
async function f() { try{ await Promise.reject('出错了') }catch(e){ return await Promise.resolve('hello world') } } f().then(v => console.log(v)) // hello world
另一种方法是await后面的Promise对象再跟一个catch方法,处理前面可能出现的错误。
async function f() { await Promise.reject('出错了') .catch(e => console.log(e)); return await Promise.resolve('hello world') } f().then(v => console.log(v)) // 出错了 // hello world
(4)错误处理
如果await后面的异步操作出错,那么等同于async函数返回的Promise对象被reject。
①使用Promise对象的catch捕获错误。
async function f() { await new Promise(function (resolve, reject) { throw new Error('出错了'); }); } f() .then(v => console.log(v)) .catch(e => console.log(e)) // Error:出错了
②将其放在try.catch代码块当中
async function f() { try { await new Promise(function (resolve, reject) { throw new Error('出错了'); }); } catch(e) { } return await('hello world'); }
如果有多个await命令,也是将其放在try...catch代码块当中。
async function main() { try { const val1 = await firstStep(); const val2 = await secondStep(val1); const val3 = await thirdStep(val1, val2); console.log('Final: ', val3); } catch (err) { console.error(err); } }
4.使用注意点
(1)把await命令放在try...catch代码块中
上面说了,await后面的Promise对象,运行结果可能是reject,所以最好把await命令放在try...catch代码块中。
async function myFunction() { try { await somethingThatReturnsAPromise(); } catch (err) { console.log(err); } } // 另一种写法 async function myFunction() { await somethingThatReturnsAPromise() .catch(function (err) { console.log(err); }); }
(2)多个await命令后面的异步操作,如果不存在继发关系,最好让它们同时触发。
如:
let foo = await getFoo()
let bar = await getBar()
上面代码中,getFoo和getBar是两个独立的异步操作,被写成了继发关系,这样比较耗时,因为只有前面完成以后,才会执行getBar,完全可以让它们同时触发。所以可以改成:
// 写法一 let [foo,bar] = await Promise.all([getFoo(),getBar()]) // 写法二 let fooPromise = getFoo() let barPromise = getBar() let foo = await fooPromise let bar = await barPromise
这样,getFoo和getBar都是同时触发,可以缩短程序执行时间
示例:两个异步都是1s出结果:
function p1f (val){ return new Promise((resolve,reject) => { setTimeout(()=>{ console.log("p1执行结束"); resolve("p1执行结束") },1000) }) } function p2f (val){ return new Promise((resolve,reject) => { setTimeout(()=>{ console.log("p2执行结束"); resolve("p2执行结束") },1000) }) } async function astncTest(){ let [p1,p2] = await Promise.all([p1f(),p2f()]) return p1 + "------" + p2 } astncTest().then(res=>{ console.log(res); }) // p1执行结束 // p2执行结束 // p1执行结束------p2执行结束
上面代码中,1s后p1f()和p2f()的结果同时输出。
(3)await命令只能用在async函数之中,如果用在普通函数,就会报错
如:
async function dbFuc(db) { let docs = [{}, {}, {}]; // 报错 docs.forEach(function (doc) { await db.post(doc); }); }
此时如果将forEach方法的参数也改成async函数也可能报错,这里改成一个简单的异步:
let a =[1,2,3,4,5] function testAsy(){ a.forEach(async function(e){ await new Promise((resolve,reject)=>{ setTimeout(()=>{ console.log(e);
resolve() },3000) }) }) } testAsy() // 1 // 2 // 3 // 4 // 5
上面代码执行后在3s后统一输出了1-5,并非一个一个打印的。正确的写法是采用for循环:
let a = [1, 2, 3, 4, 5] async function testAsy() { for (let e of a) { await new Promise((resolve, reject) => { setTimeout(() => { console.log(e); resolve() }, 1000) }) } } testAsy() // 1 // 2 // 3 // 4 // 5
这样就能实现每隔1s打印一个数字
另一种方法是使用数组的reduce()方法。
let a = [1, 2, 3, 4, 5] async function testAsy() { await a.reduce(async (_,item) => { console.log('---',item); await _; await new Promise((resolve, reject) => { setTimeout(() => { console.log(item); resolve() }, 1000) }) },undefined) } testAsy()
上面例子中,reduce()
方法的第一个参数是async
函数,导致该函数的第一个参数是前一步操作返回的 Promise 对象,所以必须使用await
等待它操作结束。另外,reduce()
方法返回的是docs
数组最后一个成员的async
函数的执行结果,也是一个 Promise 对象,导致在它前面也必须加上await
。
上面的reduce()
的参数函数里面没有return
语句,原因是这个函数的主要目的是db.post()
操作,不是返回值。而且async
函数不管有没有return
语句,总是返回一个 Promise 对象,所以这里的return
是不必要的。
如果确实希望多个请求并发执行,可以使用Promise.all
方法。当三个请求都会resolved
时,下面两种写法效果相同。
async function dbFuc(db) { let docs = [{}, {}, {}]; let promises = docs.map((doc) => db.post(doc)); let results = await Promise.all(promises); console.log(results); } // 或者使用下面的写法 async function dbFuc(db) { let docs = [{}, {}, {}]; let promises = docs.map((doc) => db.post(doc)); let results = []; for (let promise of promises) { results.push(await promise); } console.log(results); }
(4)async函数可以保留运行堆栈
const a = () => { b().then(() => c()); };
上面代码中,函数a内部运行了一个异步任务b()。当b()运行结束的时候函数a不会中断,而是继续执行。等到b()运行结束,可能a()早就运行结束了,b()所在的上下文环境已经消失了,如果b()或者c()报错,错误堆栈将不包括a()。
将其改为async函数:
const a = async () => {
await b();
c();
}
上面代码中,b()运行的时候,a()时暂停执行,上下文环境都保存着。一旦b()或c()报错,错误堆栈将包含a()