我相信, 在我们平时的开发过程当中, 大家或多或少的都会遇到要处理异步逻辑的情况, 最常见的情形比如是一个页面需要请求两个或者多个接口, 然后等这几个接口都返回之后再渲染页面, 这样的逻辑我们可以用回调函数
的方式去完成, 以及ES6
里的generator
函数, 当然啦, 还有目前大家使用最多的, 也是ES6
提供的另一个解决方案: promise
对象, 回调函数
的方式如果嵌套层数一多则会造成传说中的回调地狱
问题, 代码冗余的同时不易维护, 而generator
则被promise
的光环盖过了, 目前在使用率上个人觉得是低于promise
的, 不过蚂蚁金服
的redux
的框架dvajs还在使用, 个人推测是历史遗留问题, 但仅仅是推测, 但话又说回来, dvajs
使用generator
来解决异步逻辑并没有什么不妥, 目前这套体系已经很成熟, 使用promise
来重构duck不必
接下来就是promise
, 说起promise
想必大家都不陌生, 它是目前js
异步逻辑最成熟的解决方案之一, 但如果聊到promis
, 那么就一定离不开我们今天要聊的async/await
, 因为没有它们, promise
只是异步编程解决方案, 但有了它们才是优雅的异步编程解决方案
promise
这不是本篇的重点, 但还是稍微聊一聊, 相信大多数的朋友对于这个语法已经很熟悉了, 如有不了解的童鞋可以查阅阮一峰老师的ECMAScript 6 入门_Promise 对象, 个人认为这是一篇不可多得的学习ES6
相关语法的好文章
首先, Promise
是一个构造函数
, 需要我们使用new
操作符调用, 从而生成一个promise
对象, Promise
接收一个函数
作为参数, 同时这个函数还有两个参数, 这两个参数也是函数, 这两个函数依次是resolve
和reject
, 分别表示成功
和失败
:
const promise = new Promise(
(resolve, reject) => {
//...
if(/* 异步操作成功 */) {
//返回异步操作的结果
resolve(value);
}else{
//异步操作失败, 返回错误或者其他内容
reject(error);
}
}
)
复制代码
生成的promise
对象上有一个then
的方法, 这是成功
的回调, 还有一个catch
的方法, 这个方法则是失败
的回调, 同时then
方法会返回一个promise
对象, 意味着我们可以使用jQuery
那样的链式
写法书写我们的代码:
promise
.then(
value => {
//...
}
)
.catch(
error => {
//...
}
);
复制代码
大多数情况我们会把promise
对象当做一个函数的返回值来使用:
const handlePromise = () => (
new Promise(
(resolve, reject) => {
//...
if(/* 异步操作成功 */) {
//返回异步操作的结果
resolve(value);
}else{
//异步操作失败, 返回错误或者其他内容
reject(error);
}
}
)
);
handlePromise()
.then(
value => {
//...
}
)
.catch(
error => {
//...
}
);
复制代码
async/await
语法
接下来就是我们今天的主角: async/await
, 这两个关键字是ES8
的关键字, 配合ES6
的promies
就组成了目前js
中最优雅的异步解决方案, 在此之前我们先来看看它们的语法是怎样的, 它们的语法很简单:
async
将一般的函数变成了异步函数await
只能在异步函数
中使用, 同时它可以放在任何函数调用之前(这里主要是放在返回promise
对象的函数调用之前)- 当
await
后面的函数有返回值, 那么我们将可以使用这个函数的返回值(如果那个函数返回的是promise
对象, 则返回值是promise
resolve
返回的值, 也就是异步操作成功之后返回的值)
也就说它们是一起出现, 同时我们只能在async
函数里面使用await
关键字来接收函数的返回值
async的用法
了解了语法, 那么我们来看看它们在实际业务中的写法, 首先看看async
怎么用的, async
是asynchronous
的缩写, 异步
的意思, 它会将普通函数改为异步函数
, 写法有两种
函数声明/定义
就是使用function
关键字声明一个函数, 将async
写在函数声明
之前, 那么这个普通的函数就变成了异步函数
async function foo() {
//...
};
复制代码
然后是模块化的写法:
export async function foo() {
//...
};
复制代码
export default async function foo() {
//...
};
复制代码
我个人的习惯是在导出class
和组件
的时候使用export default
, 当只是一个util
一个工具类函数的时候则使用export
, 这个因人而异, 没有对错之分, 习惯使然, 只要注意export default
和export
的区别即可, 关于这两种导出方式的区别可以查看这篇文章: ES6模块化import export的用法
函数表达式的写法
使用了ES6
之后, 函数表达式
就变成了我们写函数时最常用的方式了, 由于没有了function
关键字, 此时异步函数
的写法就稍有不同了:
const foo = async () => {
//...
};
复制代码
相应的模块化的语法则为:
export const foo = async () => {
//...
};
复制代码
await的用法
聊完了async
的用法, 接下来我们聊聊await
的用法, await
这个单词的意思就是等待
的意思
写在普通函数调用之前
无论写在什么函数调用的前面, 都要留意它必须用在异步函数
内:
const bar = () => {
console.log(123);
}
const foo = async () => {
await bar();
};
foo();
复制代码
如果没有在async
函数中写await
, 而是在普通函数中写await
:
const bar = () => {
console.log(123);
}
const foo = () => {
await bar();
};
foo();
复制代码
此时将报错: await is only valid in async function
输出结果123
, 倘若我们用一个变量去接收bar
函数的返回值, 那结果是什么呢? 看代码我们会发现, bar
函数并没有返回值, 所以结果是undefined
:
const bar = () => {
console.log(123);
}
const foo = async () => {
const res = await bar();
console.log(res);
};
foo();
复制代码
此时先输出了123
, 然后输出了undefined
, 那如果我们让bar
函数有一个返回值呢? 比如:
const bar = () => {
console.log(123);
return 1;
}
const foo = async () => {
const res = await bar();
console.log(res);
};
foo();
复制代码
此时则是先输出123
, 然后再输出1
, 我们发现我们接收到了bar
函数的返回值, 然而平时我们在不写await
的时候也能正常的获取某个函数的返回值:
const bar = () => {
console.log(123);
return 1;
}
const foo = async () => {
const res = bar();
console.log(res);
};
foo();
复制代码
这种情况下使用await
似乎就没意义了, 这也解释了为何单独使用await
会报错, 因为它只在返回promise
的时候使用才有意义
写在promise函数调用之前
确切的说是写在返回promise
对象的函数调用之前:
const bar = () => (
new Promise(
resolve => {
resolve(123);
}
)
)
const foo = async () => {
const res = await bar();
console.log(res);
};
foo();
复制代码
此时变量res
的值是123
, 也就是说我们的await
会等待promise
执行成功
也就是执行resolve
方法, 从而接收resolve
方法的返回值, 如果不使用await
, 那么我们就只是接收到一个promise
对象而已
实现交通信号灯
回到最初的问题, async/await
究竟优雅在什么地方呢? 那我们就要先看看单独使用promise
不优雅在哪些地方, 上面的promise
的代码其实还是使用了回调函数
的写法, 也就是说如果有多个异步操作, 那么就会矫枉过正了, 会再回到我们的回调地狱
中去, 得不偿失
比如我们要实现一个交通信号灯, 3秒之后绿灯, 1秒之后黄灯, 2秒之后红灯, 依次对比两种方法
promise+then
const trafficLight = (duration, color) => (
new Promise(
resolve => {
setTimeout(
() => {
console.log(color);
resolve();
},
duration
)
}
)
)
const main = () => {
trafficLight(3000, 'green').then(
() => {
trafficLight(1000, 'yellow').then(
() => {
trafficLight(2000, 'red').then(
() => {
main();
}
)
}
)
}
)
}
main();
复制代码
看着我们的main
函数中的回调, 我整个人都开始不好了…
async/await+promise
接下来我们把上面的写法改造一下:
const trafficLight = (duration, color) => (
new Promise(
resolve => {
setTimeout(
() => {
console.log(color);
resolve();
},
duration
)
}
)
)
const main = async () => {
await trafficLight(3000, 'green');
await trafficLight(1000, 'yellow');
await trafficLight(2000, 'red');
main();
}
main();
复制代码
效果一样, 但此时我们再看main
函数中的写法, 一行接着一行, 用同步的写法实现了异步的逻辑, 看起来代码’清爽’了不少, 最重要的是逻辑更加清晰, ‘等待’一行执行结束再执行下一行, 再’等待’, 再执行…代码量更少, 更易于维护, 这便是它的优雅之处了
参考文章: