ES2017 标准引入了 async 函数,使异步操作变得更加方便。
async 函数是 Generator 函数的语法糖。
改进:
async 函数对 Generator 函数的改进主要有四点:
- 内置执行器
Generator 函数的执行必须依靠执行器,所以才有了 co模块,而async 函数自带执行器。
- 更好的语义
async和await, 比星号和yield的语义更清楚。async 表示函数里有异步操作,await 表示紧跟在后面的表达式需要等待结果。
- 更广的适用性
co模块约定,yield 命令后面只能是 Thunk 函数或promise 对象,而 async 函数的 await 命令后面,可以是Promise对象和原始类型的值(数值、字符串和布尔值,但会立即自动转为 resolved 的Promise 对象)。
- 返回值是Promise
async 函数的返回值是 Promise 对象,比 Generator 函数返回值 Iterator 对象方便。你可用 then 方法指定下一步操作。进一步说, async 函数完全可以看做多个异步操作,包装成的一个Promise 对象, 而await 命令就是内部 then 命令的语法糖。
基本用法
async 函数返回一个Promise 对象,可以使用then 方法添加回调函数。当函数执行的时候,一旦遇到await就会先返回,等到异步操作完成,再接着执行函数体内后面的语句。
async function getStockPriceName(name) {
const symbol = await getStockSymbol(name)
const stockPrice = await getStockPrice(symbol)
}
getStockPriceName('goog').then(function(result) {
console.log(result)
})
指定多少毫秒后输出一个值
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)
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 函数的语法规则总体上比较简单,难点是错误处理机制
返回Promise 对象
async 函数内部return 语法返回的值,会成为then方法回调函数的参数。
async function f() {
return 'hello world'
}
f().then(v => console.log(v))
// hello world
async 函数内部抛出错误,会被catch方法的回调函数接收。
async function f() {
throw new Error('出错了')
}
f().then(
v => console.log(v),
e => console.log(e)
)
// Error: 出错了
f().catch(e => console.log(e)) // Error: 出错了
Promise 对象的状态变化
async 函数返回的 Promise 对象,必须等到内部所有 await 命令或后面的Promise 对象执行完成,才能发生状态改变,除非遇到return 语句或抛出错误。即: async 函数内部的异步操作执行完,才会执行then方法指定的回调函数。
async function getTitle(url) {
let response = await fetch(url);
let html = await reponse.text();
return html.match(/<title>([\s\S]+)</title>/i)[1]
}
getTitle('http:www.baidu.com').then(console.log('最后执行'))
getTitle 内部有三个操作: 抓取网页、取出文本、匹配页面标题。只有这三个操作全部完成,才会执行then 方法.
await命令
正常情况下,await 命令后面是一个Promise 对象,返回该对象的结果。如果不是Promise 对象,就直接返回对应的值。
async function f() {
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 actualTime = await new Sleep(1000);
console.log(actualTime)
})()
await 命令后面的Promise 对象如果变成为 reject 状态,则reject 的参数会被catch 方法的回调函数接收到。
async function f() {
await Promise.reject('出错了')
}
f()
.then(v => console.log(v))
.catch(e => console.log(e))
// 出错了
上例中,await 语句前不管有没有return ,reject 方法的参数依然传入了catch 方法的回调函数。
任何一个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) {
console.log(e)
}
return await Promise.resolve('hello world')
}
f()
.then(v => console.log(v))
另一种方法是 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))
// return await Promise.resolve('hello world') 需要 return
// Promise.resolve('hello world') 不需要 return
错误处理
防止出错的方法,就是将其放在 try…catch 中
async function f() {
try {
await new Promise((resolve, reject) => {
throw new Error('出错了')
})
} catch (e) {
console.log(e)
}
return await('hello world')
}
多个await 命令,也可以统一放在 try … catch 结构中
async function main() {
try {
const val1 = await firstStep()
const val2 = await secondStep()
const val3 = await thirdStep()
} catch (err) {
console.log(err)
}
}
使用try.catch 结构, 实现多次重复尝试
const fetch = require('superagent')
const NUM_TIMES = 3;
async function test() {
let i;
for (i = 0; i < NUM_TIMES; ++i) {
try {
await fetch.get('http://www.test.com')
break;
} catch (err) {}
}
}
test()
如果await 操作成功,就会使用break 语句退出循环;如果失败,会被catch语句捕捉,然后进入下一轮循环。
使用注意点
一:把await 命令放在try…catch 代码块中
二:多个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;
三:await 命令只能用在async 函数中,如果用在普通函数中,会报错。
async function dgFuc(db) {
// 报错
docs.forEach(function(doc) {
await db.post(doc)
})
}
// forEach 并发关系
function dgFuc(db) {
docs.forEach(async function(doc) {
await db.post(doc)
})
}
// for..of 继发执行
async function () {
let docs = [{}, {}, {}]
for (let doc of docs) {
await db.post(doc)
}
}
需要多个请求并发执行,可用Promise.all 方法
async function dbFunc(db) {
let docs = [{}, {}, {}]
let promises = docs.map(doc => db.post(doc))
let result = Promise.all(promises)
console.log(result)
}
// 或
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)
}
esm 模块加载器支持顶层await, 即await 命令可不放在async 函数里,直接使用。
// async 函数的写法
const start = async () => {
const res = await fetch('baidu.com')
return res.text()
}
start().then(console.log(v))
// 顶层await 的写法(esm 加载器)
const res = await fetch('baidu.com')
console.log(await res.text())
四: async 函数可以保留运行栈
// b() 或 c() 报错,错误堆栈可能不包括 a()
const a = () => {
b().then(() => c())
}
// b() 或 c() 报错, 错误推展包括 a()
const a = async () => {
await b()
c()
}
async 函数的实现原理
async 函数的实现原理, 就是 将Generator 函数和自动执行器包装在一个函数里。
async、Promise、Generator函数比较
嘉定某个DOM元素上面,部署了一系列的动画,请一个动画结束,才能开始后一个个。如果当中有一个动画出错,就不再往下执行,返回上一个成功执行的动画的返回值。
Promise
function chainAnimationsPromise(elem, animations) {
let ret = null;
let p = Promise.resolve()
for (let anim of animations) {
p = p.then(function(val) {
ret = val
return anim(elem)
})
}
// 返回一个部署了错误捕捉机制的Promise
return p.catch(e => {}).then(function() {
return ret
})
}
语义不容易看出来
Generator 函数
function chainAnimationsGenerator(elem, animations) {
return spawn(function* () {
let ret = null;
try {
for(let animof animations) {
ret = yield anim(elem)
}
} catch(e) {}
return ret;
})
}
语义比Promise 写法更清晰, 但需要自己提供 spawn 自动执行器函数。
async
async function chainAnimationsAsync(elem, animations) {
let ret = null;
try {
for(let anim of animations) {
ret = await anim(elem)
}
} catch(e) {}
return ret;
}
最简洁,最符合语义。将Generator 的自动执行器,再语法上提供,不暴露给用户。
按顺序完成异步操作
如: 一次远程读取一组URL,然后按照读取的顺序输出结果
Promise
function logInOrder(urls) {
// 远程读取所有 URL
const textPromises = urls.map(ulr => {
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 的远程操作是继发,耗时。
async function logInOrder(urls) {
// 并发读取
const textPromises = urls.map(async url => {
const response = await fetch(url)
return response.text()
})
// 按次序输出
for (const textPromise of textPromises) {
console.log(await textPromise)
}
}
for await … of
for .. of 循环用来遍历同步的Iterator 接口。新引入的 for await … of 循环,则用来遍历异步的Iterator 接口。
async function f() {
for await (const x of crateAsyncIterable(['a', 'b'])) {
console.log(x)
}
}
crateAsyncIterable() 返回一个拥有异步遍历器接口对象,for … of 循环自动调用这个对象的异步遍历器的next 方法,会扽到一个Promise 对象。await 用来处理这个Promise对象,一旦resolve,把都得到的值(x) 传入 for… of 的循环体。
for await… of 循环的一个用途: 部署了 asyncIterable 操作的异步接口,可以直接放入这个循环。
let body = ""
async function f() {
fot await(const data of req) body += data;
const parsed = JSON.parse(body)
console.log('got', parsed)
}
如果next方法返回的Promise 对象被reject, fot await…of 就会报错,要用 try..catch 捕获。
async function() {
try {
for await (const x of crateRejctingIterable()) {
console.log(x)
}
} actch(e) {
console.log(e)
}
}
for await … of 循环也可以用于同步遍历器
(async function() {
for await (const x of ['a', 'b']) {
console.log(x)
}
})()