一、含义
ES2017 标准引入了 async 函数,使得异步操作变得更加方便。async 函数时 Generator 函数的语法糖
async 函数对 Generator 函数的改进体现在以下4点:
-
内置执行器
Generator 函数的执行必须靠执行器,所以才有了 co 模块,而 async 函数自带执行器。 -
更好的语义
async 和 await 比起星号 和 yield,语义更加清楚 -
更广的适用性
co 模块约定,yield 命令后面只能是 Thunk 函数或 Promise 对象,而 async 函数的 await 命令后面,可以是 Promise 对象和 原始类型的值(数值、字符串 和 布尔值,但这时等同于同步操作) -
返回的是Promise
async 函数的返回值是 Promise 对象,这比 Generator 函数的返回值是 Iterator 对象方便了许多。可以用 then 方法指定下一步的操作
二、用法
async 函数返回一个 Promise 对象,可以使用 then 方法添加回调函数。当函数执行的时候,一旦遇到 await 就会先返回等到异步操作完成,再接着执行函数体内后面的语句。
function timeout(ms) {
return new Promise( resolve => {
setTimeout(resolve, ms)
})
}
async function asyncPrint(value, ms) {
await timeout(ms)
console.log(value)
}
asyncPrint('Hello World', 2000)
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() => {}
三、语法
async 函数的语法规则总体上来说比较简单,难点是错误处理机制
3.1、返回 Promise 对象
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: 出错了
3.2、Promise 对象的状态变化
async 函数返回的 Promise 对象必须等到内部所有 await 命令后面的 Promise 对象执行完才会发生状态变化,除非遇到 return 语句或抛出错误
3.3、await 命令
正常情况下,await 命令后面是一个 Promise 对象。如果不是,会被转成一共立即 resolve 的 Promise对象
async function f() {
return await 123
}
f().then(v => console.log(v))
// 123
await 命令后面的 Promise 对象如果变为 reject 状态,则 reject的参数会被 catch 方法的回调函数接收到
async function f() {
await Promise.reject('出错了')
}
f()
.then(v => console.log(v))
.then(e => console.log(e))
// 出错了
只要一个 await 语句后面的 Promise 变为 reject,那么整个 async 函数都会中断执行
有时,我们希望即使前一共异步操作失败,,也不要中断后面的异步操作。这时可以将第一个 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
3.4 错误处理
如果 await 后面的异步操作出错,那么等同于 async 函数返回的 Promise 对象被 reject。
防止出错的方法即使用上面介绍的将其放在 try...catch 代码块之中。如果有多个 await 命令,则可以统一放在 try...catch 结构中。
使用 try...catch 结构,实现多次重复尝试。
const superagent = require('superagent')
const NUM_RETRIES = 3
async function test() {
let i;
for (i = 0; i < NUM_RETRIES; ++i) {
try {
await superagent.get('http://google.com/this-throws-an-error')
break;
} catch(err) {}
}
console.log(i) // 3
}
test()
如果 await 操作成功,则会使用 break 语句退出循环;如果失败,则会被 catch 语句捕捉,然后进入下一轮循环
3.5、使用注意点
- await 命令后面的 Promise对象的运行结果可能是 rejected,所以最好把 await 命令放在 try...catch 代码块中
async function myFuntion() {
try {
await somethingThatReturnsAPromise()
} catch(err) {
console.log(err)
}
}
// 写法二
async function myFunction() {
await somethingThatReturnsAPromise()
.catch(err => console.log(err))
}
- 多个 await 命令后面的异步操作如果不存在继发关系,最好让他们同时触发
// 写法一
let [foo, bar] = await Promise.all([getFoo(), getBar()])
// 写法二
let fooPromise = getFoo()
let barPromise = getBar()
let foo = await fooPromise
let bar = await barPromise
上面两种写法中,getFoo 和 getBar 都是同时触发,这样就会缩短程序的执行时间。
- await 命令只能在 async 函数之中,如果用来普通函数中就会报错
四、async 函数的实现原理
async 函数的实现原理就是将 Generator 函数和 自动执行器 包装在一个函数里
async function fn(args) {
// ...
}
// 等同于
function fn(args) {
return spawn(function* () {
// ...
})
}
spawn 函数的实现
function spawn(genF) {
return new Promise(function(resolve, reject) {
var gen = genF()
function step(nextF) {
try {
var next = nextF()
} catch(e) {
return reject(e)
}
if (next.done) {
return resolve(next)
}
Promise.resolve(next.value).then(function(v) {
step(function() {return gen.next(v) })
}, function(e) {
step(function() { return gen.throw(e) })
})
}
step(function() { return gen.next(undefined) })
})
}
五、其他异步处理方法的比较
假定某个 DOM 元素上面,部署了一系列的动画,前一个动画结束,才能开始后一个。如果当中有一个动画出错,就不再继续执行,而返回上一个成功执行的动画的返回值
Promise 的写法
function chainAnimationsPromise(elem, animations) {
// 变量 ret 同来保存上一个动画的返回值
var ret = null
// 新建一个空的 Promise
var p = Promise.resolve()
// 使用 then 方法,添加所有动画
for(var anim of animations) {
p = p.then(function(val) {
ret = val
return anim(elem)
})
}
}
// 返回一个不熟了 错误捕捉机制的 Promise
return p.catch(function(e) {
/* 忽略错误继续执行 */
}).then(function() {
return ret
})
相较 回调函数的写法大大改进,缺点是本身的语义不强
Generator 函数的写法
function chainAnimationsGenerator(elem, animations) {
return spawn(function* () {
var ret = null
try {
for(var anim of animations) {
ret = yield anim(elem)
}
} catch(e) {
/* 忽略错误,继续执行 */
}
return ret
})
}
语义比 Promise 写法更清晰,用户定义的操作全部都出现在 spawn 函数的内部。缺点在于,必须有一个任务运行器自动执行 Generator 函数(spawn),它返回一个Promise 对象,而且必须保证 yield 语句后面的表达式返回一个 Promise
async 函数的写法
async function chainAnimationsAsync(elem, animations) {
var ret = null
try {
for(var anim of animations) {
ret = await anim(elem)
}
} catch(e) {
/* 忽略*/
}
return ret
}
六、实例:按顺序完成异步操作
依次远程读取一组 URL,然后按照读取的顺序输出结果
Promise 的写法如下
function logInOrder(urls) {
// 远程读取所有 URL
const textPromise = urls.map(url => {
return fetch(url).then(response => response.text() )
})
// 按次序输出
textPromise.reduce((chain, textPromise) => {
return chain.then(() => textPromise)
.then(text => console.log(text))
}, Promise.resolve())
}
上面每个 fetch 操作都返回一个 Promise 对象,放入 textPromise 数组。然后,reduce 方法依次处理每个 Promise 对象,并且使用 then 将所有 Promise 对象连起来,因此就可以依次输出结果
async 函数实现
async function logInOrder(urls) {
for (const url of urls) {
const response = await fetch(url)
console.log(await response.text())
}
}
问题是所有远程操作都是继发,只有前一URL 返回结果后才会去读下一个URL,这样做效率很低,非常浪费时间。
同时发送远程请求
async function logInOrder(urls) {
// 并发读取远程 URL
const textPromises = urls.map(async url => {
const response = await fetch(url)
return response.text()
})
// 按次序输出
for (const textPromise of textPromises) {
console.log(await textPromise)
}
}
上面的代码中,虽然 map 方法的参数是 async 函数,但它是并发执行的,因为只有 async函数内部是继发执行,外部不受影响。后面的 for...of 循环内部使用了 await,因此实现了按顺序输出。








网友评论