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.