一用就能感觉到,async和await是目前处理异步逻辑最优雅的方式。

async/await 特点概要

  • 属于ES2017,应用在项目中需要babel(具体是stage-3),node最新版已经支持
  • 就是 Generator 函数的语法糖
  • 内置执行器,与普通函数一模一样,只要一行便可执行
  • async函数
    • async表示函数里有异步操作
    • async函数完全可以看作多个异步操作,包装成的一个Promise对象
    • async函数返回的是 Promise 对象,可以作为await命令的参数
    • async函数内部return语句返回的值,会成为then方法回调函数的参数
    • 只有async函数内部的异步操作执行完,才会执行then方法指定的回调函数
  • await命令
    • await表示紧跟在后面的表达式需要等待结果
    • await命令就是内部then命令的语法糖
    • 只要一个await语句后面的 Promise 变为reject,那么整个async函数都会中断执行。
      • 不希望如此则用try...catch...语句同步形式处理
      • 或者在await后的promise对象跟上catch方法

async/await 使用姿势

  • 函数声明
    • async function foo() {}
  • 函数表达式
    • const foo = async function () {};
    • const foo = async () => {};
  • 对象的方法
    let obj = { async foo() {} };
    obj.foo().then(...);
    
  • class的方法
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    class User {
    constructor() {
    super()
    }
    getUserInfo(url) {
    return fetch(url).then(res => res.user)
    }
    async foo() {
    const user_info = await this.getUserInfo(url)
    dosomething...
    }
    }
    const user = new User()
    user.foo().then(...)

async/await 基本实践

async/await最基本的用法,就是在async函数执行的时候,遇到await命令,就会等到await后的promise执行完毕,再接着执行函数体await之后的语句

这样,就会让人觉得这其实是一段同步的代码,按顺序自上而下执行。真正异步的东西其实在await部分,以及函数返回promise之后。

下面记下自己的亲自实践过程,来更好的解释async和await的用法。

业务场景是,后台给我们一组实例id,我们需要通过遍历这些id来异步向后台请求,获取任务task。(有人估计问为什么不后台直接返回过来任务task。。据后台同事说,这种前端通过遍历发送多个异步请求的效率比较高。。)

起初,为了赶工期。。是这么写的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
setTasks(process_ids) {
const render_tasks = [] // 这个数组是为了视图渲染而定义的数据
if (process_ids.length) {
process_ids.forEach(process_id =>
this.requireTask(process_id).then(task => { // requireTask是之前定义的请求task的promise对象
const task_info = {
category: task.category,
id: task.id,
name: task.name
}
render_tasks.push(task_info)
this.setState({ render_tasks })
})
)
} else {
this.setState({ render_tasks })
}
}

上面这段代码,有两处调用了setState,还是为了在异步请求之后再设置state,要是写在循环外面,就会先于异步请求执行。显得很冗余,也很蠢。。
最重要的是,循环的setState会使React组件render很多次。要是嵌套了子组件,子组件中有异步请求代码的话,也会使子组件发送很多重复的请求。

所以,这段代码既丑陋又影响性能。为了解决这个问题,尝试用async和await重构这段逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
async getTasks(process_ids) {
const render_tasks = []
if (process_ids.length) {
for (const process_id of process_ids) {
const task = await this.requireTask(process_id)
const task_info = {
category: task.category,
id: task.id,
name: task.name
}
render_tasks.push(task_info)
}
return render_tasks
}
setTasks() {
this.getTasks(process_ids).then(render_tasks => this.setState({ render_tasks }))
}

上面这段代码看起来就清晰多了,由于async函数返回的promise对象,所以可以将获取task和设置task分开来,使一个方法做一个任务,方便维护。
(上面的代码也是修改了很多次,经过了很多尝试,第一版:用函数表达式的写法写,在方法里调用。第二版:改成class的async方法,仍然在方法里setState)

但是,上面的代码还是存在一个问题,不要忘了,紧跟在await后面的表达式,需要等待结果,然后才能继续执行。所以上面for循环发的一个个requireTask请求是等上一个requireTask请求完毕后,再发起下一个请求的,也就是说,这里一系列requireTask请求是继发的。然而,每个requireTask请求是完全独立的,如果是继发的话,会比较耗时,影响性能。

所以,有了下面的版本,将继发的请求变成并发的形式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
async getTasks(process_ids) {
const render_tasks = []
if (process_ids.length) {
// 循环发送请求,保存请求结果
const taskRequires = process_ids.map(process_id => ({
promise: this.requireTask(process_id),
process_id
}))
// 循环请求结果,处理请求数据
for (const taskRequire of taskRequires) {
const task = await taskRequire.promise
const task_info = {
category: task.category,
id: task.id,
name: task.name
}
render_tasks.push(task_info)
}
}
return render_tasks
}
setTasks() {
this.getTasks(process_ids).then(render_tasks => this.setState({ render_tasks }))
}

后记

上面的代码应该还有值得优化的地方,主要参考了阮老师的ECMAScript 6 入门,其中提到的async函数的难点在于错误处理,这里还需要考虑错误的处理。在本文中就不赘述了。