Skip to content

了解 async 和 await

在我们公司的项目里,有很多需要写异步代码的地方。有时候,为了让异步代码写起来像同步代码一样,我们会选择使用 Promise,或者 async/await。这两种方式都可以让异步代码写起来像同步代码一样。

异步解决方案的发展历程

回调函数

在 ES6 诞生之前,基本上所有的异步处理都是基于回调函数函数实现的,你们可能会见过下面这种代码:

js
ajax('aaa', () => {
  // callback
  ajax('bbb', () => {
    // callback
    ajax('ccc', () => {
      // callback
    })
  })
})

在 ES6 出现之前,这种代码随处可见。它虽然解决了异步执行的问题,可随之而来的是我们常听说的回调地狱问题:

  • 调试困难,不利于维护与阅读
  • 耦合性太强,一旦某一个嵌套层级有改动,就会影响整个回调的执行

所以,为了解决这个问题,社区最早提出和实现了 Promise,ES6 将其写进了语言标准,统一了用法。

Promise

上面的回调函数可以改写成这样:

js
ajax('aaa')
  .then(res => {
    return ajax('bbb')
  }).then(res => {
    return ajax('ccc')
  })

generator 函数

Generator 函数是 ES6 提供的一种异步编程解决方案,语法行为与传统函数完全不同。

async/await

因为直接使用 Generator 函数有一些不方便,所有出现了 async/await。它是 Generator 函数的语法糖。

介绍 generator 函数

generator 函数是一个什么样的函数?

假设现在有一个普通的函数 foo:

js
function foo() {
  console.log('hello')

  return 'world'
}

只需要在 function 关键字和函数名 foo 直接加一个星号,这个函数就变成 generator 函数了。 在 generator 函数内部,可以使用 yield 关键字。

js
function* foo() {
  console.log('1')
  console.log('2')
  yield 1
  console.log('3')
  console.log('4')
  yield 2
  console.log('5')
  console.log('6')
  return '7'
}

上面只是定义了 generator 函数。现在调用它,它会返回一个拥有 next 方法的对象。

js
const hi = foo()

到目前为止,foo 函数内部的一行代码都还没有执行。

假设有下面这样一个程序,每次点击按钮都会执行一次 hi.next 方法:

vue
<script setup lang="ts">
  function* foo() {
    console.log('1')
    console.log('2')
    yield 1
    console.log('3')
    console.log('4')
    yield 2
    console.log('5')
    console.log('6')
    return
  }

  let hi = foo()

  function go() {
    const {done} = hi.next()
    if(done) {
      hi = foo()
    }
  }
</script>
<template>
  <button class="px-10 py-5 text-white bg-blue rounded-4" @click="go">点击</button>
</template>

我们点击第一次会打印 1 和 2,点击第二次会打印 3 和 4,点击第三次会打印 5 和 6,点击第四次什么都不会打印。

所以,generator 函数与普通函数相比,有什么不同呢?generator 函数它可以控制函数内部的代码是执行还是暂停。比如像上面那个例子,我们点击一次只会打印 1 和 2,不会打印 3456。要想打印 3456,只有继续点击按钮。

generator 函数的实际应用

用 generator 函数解决回调地狱

js
step1(function (value1) {
  step2(value1, function(value2) {
    step3(value2, function(value3) {
      step4(value3, function(value4) {
        // Do something with value4
      });
    });
  });
});

可以使用 generator 函数实现,这样不会出现回调地狱的问题。

js
function* longRunningTask() {
  try {
    var value2 = yield step1()
    var value3 = yield step2()
    var value4 = yield step3()
    var value5 = yield step4()
  } catch (e) {
    console.log(e, 'error')
  } 
}
const task = longRunningTask()
while() {
  var taskObj = task.next()
  if (taskObj.done) {
    break;
  }
}

但是直接使用 generator 函数解决回调地狱写法有点麻烦。

generator 函数在 dva 中的应用

dva 是一个 react 的轻量级应用框架。

在 dva 中使用 generator 函数来处理异步代码。

它有一个概念叫做 effect,含义是副作用。它是一个 generatore 函数。

你可以认为它就是 vuex 中的 actions 方法。

它长这样子:

js
app.model({
  reducers: {
    add() {}
  },
  effects: {
    *addRemote(action, { put, call }) {
      yield call(delay, 1000);
      yield put({ type: 'add' });
    }
  },
});

async 函数

实际上在代码中直接使用 generator 函数的情况还是比较少的。因为直接使用它比较麻烦,需要先调用 generator 函数拿到返回的迭代器对象,然后手动的调用 next 方法。如果我们使用的是 dva,框架帮我封装了 effect 函数,让我们可以更方便的使用 generator 函数。但要是没有框架帮我们封装呢?

es6 引入了 async 函数。async 函数其实就是 generator 函数的语法糖,任何 async 函数如果通过 babel 转换,最后都变成了 generator 函数。

async 函数的基本用法

  1. async 函数本身就是一个 Promise 对象。未调用时它的状态为 pending。
  2. 在 async 函数里使用 await 关键字,等待异步代码执行完后再继续执行下面的代码。
  3. async 函数里 return 的返回值,能够在 async 函数的 then 方法里以参数的形式拿到。

async 函数的实现原理

有这样一个 async 函数:

js
async function foo(args) {
  console.log(123)
}

用 generator 函数实现它:

js
function foo(args) {
  return chunk(function *() {
    console.log(123)
  })
}

chunk 函数接收一个 generator 函数作为参数。chunk 函数可以理解它是自动执行 generator 函数的一种方法。

chunk 函数的实现:

js
function chunk(genFn) {
  return new Promise((resolve, reject) => {
    // 调用参数 genFn,它是一个 generator 函数
    const gen = genFn()
    // 定义一个内部函数 step
    function step(nextFn) {
      let next;
      try {
        next = nextFn
      } catch(e) {
        return reject(e)
      }
      if(next.done) {
        return resolve(next.value)
      } else {
        Promise.resolve(next.value).then(value => {
          step(() => {
            return gen.next(value)
          })
        }, err => {
          step(() => {
            return gen.throw(err)
          })
        })
      }
    }
    step(() => gen.next())
  })
}

可以看到上面,async 函数不仅封装了 generator 函数,还封装了 Promise,在 chunk 函数中返回了一个 new Promise()。这也就是为什么执行 async 函数时,会返回一个 Promise 对象的原因。

如果使用 babel 转译 async 函数,就会像下面这样:

js
async function foo(args) {
  console.log(123)
}

变成

js
"use strict";

function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) {
  try {
    var info = gen[key](arg);
    var value = info.value;
  } catch (error) {
    reject(error); return;
  }
  if (info.done) {
    resolve(value);
  } else {
    Promise.resolve(value).then(_next, _throw); 
  }
}
function _asyncToGenerator(fn) {
  return function () {
    var self = this, args = arguments;
    return new Promise(function (resolve, reject) {
      var gen = fn.apply(self, args); 
      function _next(value) {
        asyncGeneratorStep(gen, resolve, reject, _next, _throw, "next", value);
      }
      function _throw(err) {
        asyncGeneratorStep(gen, resolve, reject, _next, _throw, "throw", err);
      }
      _next(undefined);
    });
  };
}
function foo(_x) {
  return _foo.apply(this, arguments);
}
function _foo() {
  _foo = _asyncToGenerator(function* (args) { 
    console.log(123);
  });
  return _foo.apply(this, arguments);
}

可以看到经过 babel 转换过后的 async 函数包含了 generator 函数和 Promise。

什么情况下需要 try/catch?

  1. 当 await 后面是 reject 状态的 Promise 时
  2. 当 await 后面是一个请求接口,可能请求失败抛出异常时
  3. 当代码可能出现异常时

不使用 try/catch 的三种方案

直接使用 .catch 捕获异常

比如

js
loading.value = true
const res = await getList()
  .catch(() => loading.value = false) 
// 如果 res 为假值,说明进入了 catch
if(!res) {
  return
}
// ...
// 请求成功后继续执行

使用 await-to-js

首先下载

shell
npm i await-to-js -S

使用

js
import to from 'await-to-js'

const [err, data] = await to(getList())
if(err) {
  console.log('出错了')
  return
}
// 继续执行后面的代码

await-to-js 源码也非常简单,只有 1 文件,22 行。

js
/**
 * @param { Promise } promise
 * @param { Object= } errorExt - Additional Information you can pass to the err object
 * @return { Promise }
 */
export function to<T, U = Error> (
  promise: Promise<T>,
  errorExt?: object
): Promise<[U, undefined] | [null, T]> {
  return promise
    .then<[null, T]>((data: T) => [null, data])
    .catch<[U, undefined]>((err: U) => {
      if (errorExt) {
        const parsedError = Object.assign({}, err, errorExt);
        return [parsedError, undefined];
      }

      return [err, undefined];
    });
}

export default to;

自己封装错误捕获函数

js
async function errorCapture(p) {
  try {
    const res = await p
    return [null, res]
  } catch (e) {
    return [e, null]
  }
}

其它

在 Promise 执行器里写 async

这种写法个人认为有点多余。

处理并发请求

js
async function req1() {
  //
}
async function req2() {
  //
}
async function req3() {
  //
}
js
req1().then(res => {
  this.info = res
})
req2().then(res => {
  this.id = res.id
})
req3().then(res => {
  this.avatar = res.avatar
})
js
Promise([req1(), req2(), req3()]).then(res => {
  this.info = res[0]
  this.id = res[1].id
  this.avatar = res[2].avatar
})

处理顺序请求

js
await req1()
await req2()
await req3()

不要在 forEach 里使用 async 函数

不要将 async 函数作为 forEach 的回调函数。因为 forEach 期望的是一个同步函数,它不会等待 Promise 的状态发送改变,才去执行下一个回调函数。

Released under the MIT License.