async函数
本系列属于阮一峰老师所著的ECMAScript 6 入门学习笔记
概念
ES2017中引入了async函数,使异步操作变得更加方便。async是Generator函数的语法糖。
const gen = function* (){
const f1 = yield readFile('/etc/fstab')
const f2 = yield readFile('/etc/shells')
}
// 写成async函数
const gen = async function (){
const f1 = await readFile('/etc/fstab')
const f2 = await readFile('/etc/shells')
}
比较可知,async函数将Generator函数的*替换成async,将yield替换成await ,仅此而已
async函数对Generator函数的改进有以下四点:
(1)内置执行器
Generator函数执行需要依靠执行器,而async函数自带执行器。也就是说async函数的执行与普通函数一样
gen()
(2)更好的语义化
async和await比起*和yield,语义更加清楚。async表示函数有异步操作,await表示紧跟在后面的表达式需要等待结果
(3)更广的适用性
co模块约定,yield命令后面只能是Thunk函数或者Promise对象,而async函数的await命令后面可以是Promise对象和原始类型的值(数值、字符串和布尔值,但这等同于同步操作)
(4)返回值是Promise
async函数的返回值是Promise对象,这比Generator函数的返回值是Iterator对象方便多了,我们可以用then指定下一步的操作
基本用法
async函数会返回一个Promise对象,可以使用then方法添加回调函数。当函数执行的时候,一旦遇到await就会先返回,等到异步操作完成,再接着执行函数体后面的语句。
function timeout(ms){
return new Promise(resolve =>{
setTimeout(resolve,ms)
})
}
// 由于async函数返回的是Promise函数,因此可改写为
async function timeout(ms){
await new Promise(resolve =>{
setTimeout(resolve,ms)
})
}
async function asyncPrint(value,ms){
await timeout(ms)
console.log(value)
}
asyncPrint('hello world',50) // 在50毫秒之后输出hello world
// 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 () => {}
返回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(v),e => console.log(e)) // Error:出错了
// async函数返回的Promise对象,必须等内部所有的await命令后面的Promise对象执行完,才会发生状态改变,除非遇到return语句或者抛出错误
await命令
// 正常情况下,await命令后面是一个Promise对象。如果不是,会被转成一个立即resolve的Promise对象
async function f(){
return await 123
}
f().then(v => console.log(v))
// await后面的Promise对象变为reject状态,则reject的参数会被catch方法的回调函数接收到
async function f(){
await Promise.reject('出错了')
}
f().then(v => console.log(v)).catch(e => console.log(e)) // 出错了
// 只要有一个await语句后面的Promise变为reject,那么整个async函数都会中断执行
async function f(){
await Promise.reject('出错了')
await Promise.resolve('Hello world') // 不会执行
}
// 如果希望前一个异步操作失败,也不中断后面的异步操作,这时可以把第一个await放在try...catch结构里面,这样无论这个异步操作是否成功,第二个await都会执行
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
// 如果有多个await命令,可以统一放在try...catch结构中
使用注意点
(1)做好错误处理,最好把await命令放在try...catch代码块中
(2)多个await命令后面的异步操作,如果不存在继发关系,最好让它们同时触发
let foo = await getFoo()
let bar = await getBar()
// 这两个独立的异步操作互不影响,被写成继发关系,这样比较耗时,可以让他们同时触发,缩短程序的执行时间
// 写法一
let [foo,bar] = await Promise.all([getFoo(),getBar()])
// 写法二
let fooPromise = getFoo()
let barPromise = getBar()
let foo = await fooPromise
let bar = await barPromise
(3)await只能用在async函数中,如果用在普通函数,就会报错
async函数的实现原理
async函数的实现原理,就是将Generator函数和自动执行器,包装在一个函数里
async function fn(args){
// ...
}
// 等同于
function fn(args){
return spawn(function* (){
// ...
})
}
// 其中spawn函数就是自动执行器
与其他异步方法的比较
以下例子比较async函数、Promise、Generator函数
// 某个DOM元素,部署了一系列的动画,前一个动画结束,才开始后一个。如果其中有一个动画出错,就不再继续执行了,返回上一个成功执行的动画的返回值
// Promise的写法
function chainAnimationsPromise(elem,animations){
// 变量ret用来保存上一个动画的返回值
let ret = null
// 新建一个空的Promise
let p = Promise.resolve()
// 使用then方法,添加所有动画
for(let anim of animations){
p = p.then(function(val){
ret = val
return anim(elem)
})
}
// 返回一个部署了错误机制的Promise
return p.catch(function(e){
// 忽略错误,继续执行
}).then(function(){
return ret
})
}
// Promise写法已经比回调函数的写法大大改进,但操作本身的语义不太容易看出来
// Generator函数的写法
function chainAnimationsGenerator(elem,animations){
return spawn(function* (){
let ret = null
try{
for(let anim of animations){
ret = yield anim(elem)
}
}catch(e){
// 忽略错误,继续执行
}
return ret
})
}
// Generator函数需要一个spawn自动执行器,而且必须保证yield语句后面的表达式必须返回一个Promise
// async函数的写法
async function chainAnimationsAsync(elem,animations){
let ret = null
try{
for(let anim of animations){
ret = await anim(elem)
}
}catch(e){
//忽略错误,继续执行
}
return ret
}
// async函数的实现最简洁,最符合语义,代码量少
实例:按顺序完成异步操作
// 一次远程读取一组URL,按照读取的顺序输出结果
// Promise的写法
function logInOrder(urls){
// 远程读取所有URL
const textPromises = urls.map(url =>{
return fetch(url.then(response => response.text()))
})
// 按次序输出
textPromises.reduce((chain,textPromise) => {
return chain.then(() => textPromise).then(text => console.log(text))
},Promise.resolve())
}
// async函数写法
async function logInOrder(urls){
for(const url of urls){
const response = await fetch(url)
console.log(await response.text())
}
}
// 这样简化了写法,但是所有操作都是继发,效率差,我们需要并发请求
async function logInOrder(urls){
// 并发读取远程URL
const textPromises = urls.map(async url =>{
const response = await fetch(url)
return reponse.text()
})
// 按次序输出
for(const textPromise of textPromises){
console.log(await textPromise)
}
}
// map方法的参数是async函数,但他是并发执行的,因为只有async函数内部是继发执行,外部不受影响。后面的for...of循环内部使用await,因此实现了按顺序输出

浙公网安备 33010602011771号