ES2017 标准引入了 async 函数,使异步操作变得更加方便。
async 函数是 Generator 函数的语法糖。

改进:

async 函数对 Generator 函数的改进主要有四点:

  1. 内置执行器

Generator 函数的执行必须依靠执行器,所以才有了 co模块,而async 函数自带执行器。

  1. 更好的语义

async和await, 比星号和yield的语义更清楚。async 表示函数里有异步操作,await 表示紧跟在后面的表达式需要等待结果。

  1. 更广的适用性

co模块约定,yield 命令后面只能是 Thunk 函数或promise 对象,而 async 函数的 await 命令后面,可以是Promise对象和原始类型的值(数值、字符串和布尔值,但会立即自动转为 resolved 的Promise 对象)。

  1. 返回值是Promise

async 函数的返回值是 Promise 对象,比 Generator 函数返回值 Iterator 对象方便。你可用 then 方法指定下一步操作。进一步说, async 函数完全可以看做多个异步操作,包装成的一个Promise 对象, 而await 命令就是内部 then 命令的语法糖。

基本用法

async 函数返回一个Promise 对象,可以使用then 方法添加回调函数。当函数执行的时候,一旦遇到await就会先返回,等到异步操作完成,再接着执行函数体内后面的语句。

  1. async function getStockPriceName(name) {
  2. const symbol = await getStockSymbol(name)
  3. const stockPrice = await getStockPrice(symbol)
  4. }
  5. getStockPriceName('goog').then(function(result) {
  6. console.log(result)
  7. })

指定多少毫秒后输出一个值

  1. async function timeout(ms) {
  2. await new Promise(resolve => {
  3. setTimeout(resolve, ms);
  4. })
  5. }
  6. async function asyncPrint(value, ms) {
  7. await timeout(ms)
  8. console.log(value)
  9. }
  10. asyncPrint('hello world', 50)

async 函数的多种使用方式

  1. // 函数声明
  2. async function foo() {}
  3. // 函数表达式
  4. const foo = async function() {}
  5. // 对象的方法
  6. let obj = { async foo() {} }
  7. obj.foo().then()
  8. // class 的方法
  9. class Storage {
  10. constructor() {
  11. this.cachePromise = caches.open('avatars')
  12. }
  13. async getAvatar(name) {
  14. const cache = await this.cachePromise;
  15. return cache.match(`/avatars/${name}.jpg`)
  16. }
  17. }
  18. const storage = new Storage()
  19. storage.getAvatar('jake').then()
  20. // 箭头函数
  21. const foo = async () => {}

语法

async 函数的语法规则总体上比较简单,难点是错误处理机制

返回Promise 对象

async 函数内部return 语法返回的值,会成为then方法回调函数的参数。

  1. async function f() {
  2. return 'hello world'
  3. }
  4. f().then(v => console.log(v))
  5. // hello world

async 函数内部抛出错误,会被catch方法的回调函数接收。

  1. async function f() {
  2. throw new Error('出错了')
  3. }
  4. f().then(
  5. v => console.log(v),
  6. e => console.log(e)
  7. )
  8. // Error: 出错了
  9. f().catch(e => console.log(e)) // Error: 出错了

Promise 对象的状态变化

async 函数返回的 Promise 对象,必须等到内部所有 await 命令或后面的Promise 对象执行完成,才能发生状态改变,除非遇到return 语句或抛出错误。即: async 函数内部的异步操作执行完,才会执行then方法指定的回调函数。

  1. async function getTitle(url) {
  2. let response = await fetch(url);
  3. let html = await reponse.text();
  4. return html.match(/<title>([\s\S]+)</title>/i)[1]
  5. }
  6. getTitle('http:www.baidu.com').then(console.log('最后执行'))

getTitle 内部有三个操作: 抓取网页、取出文本、匹配页面标题。只有这三个操作全部完成,才会执行then 方法.

await命令

正常情况下,await 命令后面是一个Promise 对象,返回该对象的结果。如果不是Promise 对象,就直接返回对应的值。

  1. async function f() {
  2. return await 123;
  3. }
  4. f().then(v => console.log(v)) // 123

await 命令后面是一个thenable 对象(即定义then 方法的对象),那么await 也会返回一个Promise对象。

  1. class Sleep {
  2. constructor(timeout) {
  3. this.timeout = timeout
  4. }
  5. then(resolve, reject) {
  6. const startTime = Date.now()
  7. setTimeout(
  8. () => resolve(Date.now() - startTime),
  9. this.timeout
  10. )
  11. }
  12. }
  13. (async () => {
  14. const actualTime = await new Sleep(1000);
  15. console.log(actualTime)
  16. })()

await 命令后面的Promise 对象如果变成为 reject 状态,则reject 的参数会被catch 方法的回调函数接收到。

  1. async function f() {
  2. await Promise.reject('出错了')
  3. }
  4. f()
  5. .then(v => console.log(v))
  6. .catch(e => console.log(e))
  7. // 出错了

上例中,await 语句前不管有没有return ,reject 方法的参数依然传入了catch 方法的回调函数。

任何一个await语句后的Promise 对象变成reject状态,那么整个async 函数都会中断执行。

  1. async function f() {
  2. await Promise.reject('出错了')
  3. await Promise.resolve('hello world') // 不会执行
  4. }

将前一个await 放到 try…catch 结果里,不管前一个异步操作成功与否,第二个await 都会执行。

  1. async function f() {
  2. try {
  3. await Promise.reject('出错了')
  4. } catch(e) {
  5. console.log(e)
  6. }
  7. return await Promise.resolve('hello world')
  8. }
  9. f()
  10. .then(v => console.log(v))

另一种方法是 await 后面的Promise 对象带上 catch 方法,处理前面可能出现的错误。

  1. async function f() {
  2. await Promise.reject('出错了')
  3. .catch(e => console.log(e))
  4. return await Promise.resolve('hello world')
  5. }
  6. f()
  7. .then(v => console.log(v))
  8. // return await Promise.resolve('hello world') 需要 return
  9. // Promise.resolve('hello world') 不需要 return

错误处理

防止出错的方法,就是将其放在 try…catch 中

  1. async function f() {
  2. try {
  3. await new Promise((resolve, reject) => {
  4. throw new Error('出错了')
  5. })
  6. } catch (e) {
  7. console.log(e)
  8. }
  9. return await('hello world')
  10. }

多个await 命令,也可以统一放在 try … catch 结构中

  1. async function main() {
  2. try {
  3. const val1 = await firstStep()
  4. const val2 = await secondStep()
  5. const val3 = await thirdStep()
  6. } catch (err) {
  7. console.log(err)
  8. }
  9. }

使用try.catch 结构, 实现多次重复尝试

  1. const fetch = require('superagent')
  2. const NUM_TIMES = 3;
  3. async function test() {
  4. let i;
  5. for (i = 0; i < NUM_TIMES; ++i) {
  6. try {
  7. await fetch.get('http://www.test.com')
  8. break;
  9. } catch (err) {}
  10. }
  11. }
  12. test()

如果await 操作成功,就会使用break 语句退出循环;如果失败,会被catch语句捕捉,然后进入下一轮循环。

使用注意点

一:把await 命令放在try…catch 代码块中
二:多个await 命令后面的异步操作,如果不存在继发关系,最好让它们同时触发。

  1. // 继发关系
  2. let foo = await getFoo()
  3. let bar = await getBar()
  4. // 并发关系(缩短执行时间)
  5. let [foo, bar] = await Promise.all([getFoo(), getBar()])
  6. let fooPromise = getFoo()
  7. let barPromise = getBar()
  8. let foo = await fooPromise;
  9. let bar = await barPromise;

三:await 命令只能用在async 函数中,如果用在普通函数中,会报错。

  1. async function dgFuc(db) {
  2. // 报错
  3. docs.forEach(function(doc) {
  4. await db.post(doc)
  5. })
  6. }
  7. // forEach 并发关系
  8. function dgFuc(db) {
  9. docs.forEach(async function(doc) {
  10. await db.post(doc)
  11. })
  12. }
  13. // for..of 继发执行
  14. async function () {
  15. let docs = [{}, {}, {}]
  16. for (let doc of docs) {
  17. await db.post(doc)
  18. }
  19. }

需要多个请求并发执行,可用Promise.all 方法

  1. async function dbFunc(db) {
  2. let docs = [{}, {}, {}]
  3. let promises = docs.map(doc => db.post(doc))
  4. let result = Promise.all(promises)
  5. console.log(result)
  6. }
  7. // 或
  8. async function dbFuc(db) {
  9. let docs = [{}, {}, {}]
  10. let promises = docs.map(doc => db.post(doc))
  11. let results = []
  12. for (let promise of promises) {
  13. results.push(await promise)
  14. }
  15. console.log(results)
  16. }

esm 模块加载器支持顶层await, 即await 命令可不放在async 函数里,直接使用。

  1. // async 函数的写法
  2. const start = async () => {
  3. const res = await fetch('baidu.com')
  4. return res.text()
  5. }
  6. start().then(console.log(v))
  7. // 顶层await 的写法(esm 加载器)
  8. const res = await fetch('baidu.com')
  9. console.log(await res.text())

四: async 函数可以保留运行栈

  1. // b() 或 c() 报错,错误堆栈可能不包括 a()
  2. const a = () => {
  3. b().then(() => c())
  4. }
  5. // b() 或 c() 报错, 错误推展包括 a()
  6. const a = async () => {
  7. await b()
  8. c()
  9. }

async 函数的实现原理

async 函数的实现原理, 就是 将Generator 函数和自动执行器包装在一个函数里。

async、Promise、Generator函数比较

嘉定某个DOM元素上面,部署了一系列的动画,请一个动画结束,才能开始后一个个。如果当中有一个动画出错,就不再往下执行,返回上一个成功执行的动画的返回值。

Promise

  1. function chainAnimationsPromise(elem, animations) {
  2. let ret = null;
  3. let p = Promise.resolve()
  4. for (let anim of animations) {
  5. p = p.then(function(val) {
  6. ret = val
  7. return anim(elem)
  8. })
  9. }
  10. // 返回一个部署了错误捕捉机制的Promise
  11. return p.catch(e => {}).then(function() {
  12. return ret
  13. })
  14. }

语义不容易看出来

Generator 函数

  1. function chainAnimationsGenerator(elem, animations) {
  2. return spawn(function* () {
  3. let ret = null;
  4. try {
  5. for(let animof animations) {
  6. ret = yield anim(elem)
  7. }
  8. } catch(e) {}
  9. return ret;
  10. })
  11. }

语义比Promise 写法更清晰, 但需要自己提供 spawn 自动执行器函数。

async

  1. async function chainAnimationsAsync(elem, animations) {
  2. let ret = null;
  3. try {
  4. for(let anim of animations) {
  5. ret = await anim(elem)
  6. }
  7. } catch(e) {}
  8. return ret;
  9. }

最简洁,最符合语义。将Generator 的自动执行器,再语法上提供,不暴露给用户。

按顺序完成异步操作

如: 一次远程读取一组URL,然后按照读取的顺序输出结果
Promise

  1. function logInOrder(urls) {
  2. // 远程读取所有 URL
  3. const textPromises = urls.map(ulr => {
  4. return fetch(url).then(response => response.text())
  5. })
  6. // 按次序输出
  7. textPromises.reduce((chain, textPromise) => {
  8. return chain.then(() => textPromise)
  9. .then(text => console.log(text))
  10. }, Promise.resolve())
  11. }

async

  1. async function logInOrder(urls) {
  2. for (const url of urls) {
  3. const response = await fetch(url)
  4. console.log(await response.text())
  5. }
  6. }

上例, async 的远程操作是继发,耗时。

  1. async function logInOrder(urls) {
  2. // 并发读取
  3. const textPromises = urls.map(async url => {
  4. const response = await fetch(url)
  5. return response.text()
  6. })
  7. // 按次序输出
  8. for (const textPromise of textPromises) {
  9. console.log(await textPromise)
  10. }
  11. }

for await … of

for .. of 循环用来遍历同步的Iterator 接口。新引入的 for await … of 循环,则用来遍历异步的Iterator 接口。

  1. async function f() {
  2. for await (const x of crateAsyncIterable(['a', 'b'])) {
  3. console.log(x)
  4. }
  5. }

crateAsyncIterable() 返回一个拥有异步遍历器接口对象,for … of 循环自动调用这个对象的异步遍历器的next 方法,会扽到一个Promise 对象。await 用来处理这个Promise对象,一旦resolve,把都得到的值(x) 传入 for… of 的循环体。

for await… of 循环的一个用途: 部署了 asyncIterable 操作的异步接口,可以直接放入这个循环。

  1. let body = ""
  2. async function f() {
  3. fot await(const data of req) body += data;
  4. const parsed = JSON.parse(body)
  5. console.log('got', parsed)
  6. }

如果next方法返回的Promise 对象被reject, fot await…of 就会报错,要用 try..catch 捕获。

  1. async function() {
  2. try {
  3. for await (const x of crateRejctingIterable()) {
  4. console.log(x)
  5. }
  6. } actch(e) {
  7. console.log(e)
  8. }
  9. }

for await … of 循环也可以用于同步遍历器

  1. (async function() {
  2. for await (const x of ['a', 'b']) {
  3. console.log(x)
  4. }
  5. })()