1. 含义
async/await在es2017被引入,作为Generator函数的语法糖。
最主要的变化,就是将Generator的*号替换成了async,将yield替换成了await。
const asyncFn = async () =>{
const a = await 1;
const b = await 2;
console.log(a , b)
}
asyncFn() //1 2
const genFn = function* (){
yield 1;
yield 2;
}
const it = genFn();
console.log(it.next())
console.log(it.next())
2.优势
async函数对于Generator函数的改进主要体现在以下方面:
(1)内置执行器
Generator的执行依赖于执行器(因此才有了co模块)。而async函数自带执行器,也就是说async的执行和普通函数一样,直接写就可以了。
asyncFn();
(2)更好的语义
async函数的语义更加清晰易懂:async表示函数里有异步操作,await用于等待一个Promise兑现并获取它兑现之后的值。
(3)更广的适用性
co模块中,yield命令后面只能是thunk函数或者Promise对象,而await后面可以是原始类型,Promise对象和thenable对象等等。(原始类型的值会被自动转成立即resolved的Promise 对象)
(4)返回值更好处理
Generator返回迭代器,而async函数返回Promise对象,因此可以方便地利用then方法指定下一步的操作。
3.用法
async函数返回一个Promise对象,可以用then添加回调函数。
当async函数执行时,一旦遇到await函数就进入等待,等待await后的Promise对象(基础类型会被resolve成Promise)兑现后再继续执行。
例如这个例子,执行asyncFn时遇到await表达式,就要先暂停执行,等待2s到后面的Promise实例兑现后await表达式的值变成123,再继续执行asyncFn打印出来。
const promiseFn = val => {
return new Promise((resolve , reject) =>{
setTimeout(()=>{
resolve(val);
} , 2000)
})
}
const asyncFn = async () =>{
const a = await promiseFn(123);
console.log(a);
}
asyncFn()//两秒后打印123
放在then方法里处理也是同理
注意,只有async函数内部的异步操作执行完,才会执行then方法指定的回调函数
const asyncFn = async () =>{
const a = await promiseFn(123);
return a;
}
asyncFn().then(val => console.log(val))
捕获错误也是同理,都是基础的Promise操作
const asyncFn = async () =>{
throw new Error('sorry')
}
asyncFn().then(val => console.log(val) , err => console.log(err))
await后面也可以是thenable对象(定义了then方法的对象)
class Sleep{
constructor(time){
this.time = time;
}
then(resolve , reject){
const startTime = Date.now();
setTimeout(()=>{
resolve(Date.now() - startTime);
} , this.time)
}
}
const asyncFn = async () =>{
const sleepTime = await new Sleep(1000);
console.log(sleepTime);
}
asyncFn() //1000左右 会有几微秒的误差
也可以利用async/await实现sleep功能
const sleep = (delay) =>{
return new Promise((resolve , reject) =>{
setTimeout(()=>{
resolve();
} , delay)
})
}
const fn = async() =>{
for(let i = 1 ; i<=5 ; i++){
await sleep(1000);
console.log(i);
}
}
fn(); // 间隔1s输出1 2 3 4 5
还可以实现能够cancel的sleep函数
function cancellableSleep(millis) {
let timeoutId = null;
const promise = new Promise((resolve) => {
timeoutId = setTimeout(resolve, millis);
});
const cancelFn = () => {
clearTimeout(timeoutId);
};
return { promise, cancelFn };
}
// 使用示例
const { promise, cancelFn } = cancellableSleep(1000);
promise.then(() => console.log('Finished'));
setTimeout(cancelFn, 500); // 能够取消
setTimeout(cancelFn, 1500); // 不能取消
如果一个await语句后面的Promise对象变成reject状态,那么async函数就会被中断
async function f() {
await Promise.reject('出错了');
await Promise.resolve('hello world'); // 不会执行
}
可以通过try-catch代码块解决
const fn = async () =>{
try{
await Promise.reject('sorry');
}catch(e){}
return await Promise.resolve(123)
}
fn()
.then(val => console.log(val)) //123
或者直接在await后面catch
async function f() {
await Promise.reject('出错了')
.catch(e => console.log(e));
return await Promise.resolve(123);
}
f()
.then(v => console.log(v))
// 出错了
// 123
可以通过try-catch实现多次尝试fetch某网址,成功了就退出。
比如fetch自己的博客,一次就成功了直接退出,打印0。
async function test() {
let i;
for (i = 0; i < 3; ++i) {
try {
await fetch('http://120.25.100.22:12347/');
break;
} catch (err) { }
}
console.log(i); // 3
}
test(); //0
4.使用注意点
(1)await后面的Promise对象如果可能是rejected状态,最好放在try-catch中(示例在上面)
(2)多个await命令后面的异步操作,如果不存在继发关系,最好让它们同时触发。
比如这里getFoo()和getBar()是两个不相关的异步操作
let foo = await getFoo();
let bar = await getBar();
如果写成顺序继发关系会比较耗时,因为只有等getFoo()这个异步操作完成后才轮到getBar(),最好让他们同时触发。
// 写法一
let [foo, bar] = await Promise.all([getFoo(), getBar()]);
// 写法二
let fooPromise = getFoo();
let barPromise = getBar();
let foo = await fooPromise;
let bar = await barPromise;
(3)注意await不要放到普通函数/forEach中使用
forEach里的函数参数是个普通函数
async function dbFuc(db) {
let docs = [{}, {}, {}];
// 报错
docs.forEach(function (doc) {
await db.post(doc);
});
但是就算将forEach方法改成async函数也会出错,因为async/await语句在forEach中不能正常运行。
原因见这里
let sum = 0
const arr = [1, 2, 3]
async function sumFn(a, b) {
return a + b
}
function main(array) {
array.forEach(async item => {
sum = await sumFn(sum, item)
})
console.log(sum) // 0, Why?
}
main(arr)
因此最好用for...of来代替
let sum = 0
const arr = [1, 2, 3]
async function sumFn(a, b) {
return a + b
}
// await 要放在 async 函数中
async function main(array) {
for (let item of array) {
sum = await sumFn(sum, item)
}
console.log(sum) // 6
}
main(arr)
5.和Promise实例对比
实例一
Promise
const foo = (name) =>{
return new Promise((resolve , reject) =>{
console.log(name , 1);
resolve(console.log(name , 2))
})
.then(() => console.log(name , 3))
}
foo('apple');
foo('banana');
//apple 1
//apple 2
//banana 1
//banana 2
//apple 3
//banana 3
async/await
const foo = async (name) =>{
console.log(name , 1);
await console.log(name , 2);
console.log(name , 3);
}
foo('apple');
foo('banana');
//结果同上
实例二
Promise
function logInOrder(urls) {
// 远程读取所有URL
const textPromises = urls.map(url => {
return fetch(url).then(response => response.text());
});
// 按次序输出
textPromises.reduce((chain, textPromise) => {
return chain.then(() => textPromise)
.then(text => console.log(text));
}, Promise.resolve());
}
async/await继发版本
async function logInOrder(urls) {
for (const url of urls) {
const response = await fetch(url);
console.log(await response.text());
}
}
async/await并发优化版本
async function logInOrder(urls) {
// 并发读取远程URL
const textPromises = urls.map(async url => {
const response = await fetch(url);
return response.text();
});
// 按次序输出
for (const textPromise of textPromises) {
console.log(await textPromise);
}
}
6.顶层await
在模块的顶层,可以单独使用await异步函数的外面)。也就是说一个模块如果包含用了await的子模块,该模块就会等待该子模块,这一过程并不会阻塞其它子模块。
下面是一个在export表达式中使用了Fetch API的例子。任何文件只要导入这个模块,后面的代码就会等待,直到fetch完成。
// fetch request
const colors = fetch("../data/colors.json").then((response) => response.json());
export default await colors;
7. for await of
使用for await of遍历时,会等待前一个Promise对象的状态改变后,再遍历到下一个成员。
function Gen(time){
return new Promise((resolve , reject) =>{
setTimeout(() =>{
resolve(time)
} , time)
})
}
async function test(){
let arr = [Gen(1000) , Gen(2000) , Gen(3000)];
for await(let item of arr){
console.log(Date.now() , item);
}
}
test();
//1669562023046 1000
//1669562024049 2000
//1669562025051 3000