了解 async 和 await
在我们公司的项目里,有很多需要写异步代码的地方。有时候,为了让异步代码写起来像同步代码一样,我们会选择使用 Promise,或者 async/await。这两种方式都可以让异步代码写起来像同步代码一样。
异步解决方案的发展历程
回调函数
在 ES6 诞生之前,基本上所有的异步处理都是基于回调函数函数实现的,你们可能会见过下面这种代码:
ajax('aaa', () => {
// callback
ajax('bbb', () => {
// callback
ajax('ccc', () => {
// callback
})
})
})在 ES6 出现之前,这种代码随处可见。它虽然解决了异步执行的问题,可随之而来的是我们常听说的回调地狱问题:
- 调试困难,不利于维护与阅读
- 耦合性太强,一旦某一个嵌套层级有改动,就会影响整个回调的执行
所以,为了解决这个问题,社区最早提出和实现了 Promise,ES6 将其写进了语言标准,统一了用法。
Promise
上面的回调函数可以改写成这样:
ajax('aaa')
.then(res => {
return ajax('bbb')
}).then(res => {
return ajax('ccc')
})generator 函数
Generator 函数是 ES6 提供的一种异步编程解决方案,语法行为与传统函数完全不同。
async/await
因为直接使用 Generator 函数有一些不方便,所有出现了 async/await。它是 Generator 函数的语法糖。
介绍 generator 函数
generator 函数是一个什么样的函数?
假设现在有一个普通的函数 foo:
function foo() {
console.log('hello')
return 'world'
}只需要在 function 关键字和函数名 foo 直接加一个星号,这个函数就变成 generator 函数了。 在 generator 函数内部,可以使用 yield 关键字。
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 方法的对象。
const hi = foo()到目前为止,foo 函数内部的一行代码都还没有执行。
假设有下面这样一个程序,每次点击按钮都会执行一次 hi.next 方法:
<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 函数解决回调地狱
step1(function (value1) {
step2(value1, function(value2) {
step3(value2, function(value3) {
step4(value3, function(value4) {
// Do something with value4
});
});
});
});可以使用 generator 函数实现,这样不会出现回调地狱的问题。
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 方法。
它长这样子:
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 函数的基本用法
- async 函数本身就是一个 Promise 对象。未调用时它的状态为 pending。
- 在 async 函数里使用 await 关键字,等待异步代码执行完后再继续执行下面的代码。
- async 函数里 return 的返回值,能够在 async 函数的 then 方法里以参数的形式拿到。
async 函数的实现原理
有这样一个 async 函数:
async function foo(args) {
console.log(123)
}用 generator 函数实现它:
function foo(args) {
return chunk(function *() {
console.log(123)
})
}chunk 函数接收一个 generator 函数作为参数。chunk 函数可以理解它是自动执行 generator 函数的一种方法。
chunk 函数的实现:
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 函数,就会像下面这样:
async function foo(args) {
console.log(123)
}变成
"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?
- 当 await 后面是 reject 状态的 Promise 时
- 当 await 后面是一个请求接口,可能请求失败抛出异常时
- 当代码可能出现异常时
不使用 try/catch 的三种方案
直接使用 .catch 捕获异常
比如
loading.value = true
const res = await getList()
.catch(() => loading.value = false)
// 如果 res 为假值,说明进入了 catch
if(!res) {
return
}
// ...
// 请求成功后继续执行使用 await-to-js
首先下载
npm i await-to-js -S使用
import to from 'await-to-js'
const [err, data] = await to(getList())
if(err) {
console.log('出错了')
return
}
// 继续执行后面的代码await-to-js 源码也非常简单,只有 1 文件,22 行。
/**
* @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;自己封装错误捕获函数
async function errorCapture(p) {
try {
const res = await p
return [null, res]
} catch (e) {
return [e, null]
}
}其它
在 Promise 执行器里写 async
这种写法个人认为有点多余。

处理并发请求
async function req1() {
//
}
async function req2() {
//
}
async function req3() {
//
}req1().then(res => {
this.info = res
})
req2().then(res => {
this.id = res.id
})
req3().then(res => {
this.avatar = res.avatar
})Promise([req1(), req2(), req3()]).then(res => {
this.info = res[0]
this.id = res[1].id
this.avatar = res[2].avatar
})处理顺序请求
await req1()
await req2()
await req3()不要在 forEach 里使用 async 函数
不要将 async 函数作为 forEach 的回调函数。因为 forEach 期望的是一个同步函数,它不会等待 Promise 的状态发送改变,才去执行下一个回调函数。