Skip to content

JS 循环中使用 async 与 await

1. for 循环 中使用 await

for循环里面的异步操作,一般都是并发操作

若给for循环里面发异步操作前加上await关键字,则会变成串行操作,前提是for应该在async函数里面。

例如:

js
// 异步
let fn = () => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve();
    }, 1000);
  });
};


async function fn1() {
  console.log("Start");
  let arr = [27, 0, 14];

  for (var i = 0; i < arr.length; i++) {
    await fn();
    console.log(arr[i]);
  }

  console.log("End");
}

fn1();

29748935e9cdb02f9d988a8.webp

这种行为适用于大多数循环(比如whilefor of for in循环)

2. 在 forEach 循环中使用 await

for循环写起来很繁琐,是否很多时候会不经意想用forEach来达到相同的效果,但是,真的可以吗?

错误示例:

js
let arr = [27, 0, 14];

console.log("Start");

arr.forEach(async (item) => {
  await fn();
  console.log(item);
});

console.log("End");

得到的结果不是想要的 297489371cdb6c956793115.webp

JavaScript 中的 forEach不支持 promise 感知,也支持 asyncawait,所以不能在 forEach 使用 await

3. 在 map 中使用 await

map方法会返回一个由原数组每个元素执行回调函数的结果组成的新数组,如果在map中使用await, map 始终返回promise数组,这是因为异步函数总是返回promise。并且李敏每个promise都是pending状态。

js
// 异步
let fn = (arg) => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(arg);
    }, 1000);
  });
}; 

let arr = [27, 0, 14];

async function fn1() {
  console.log("Start");

  let resuPromises = arr.map(async (item) => {
    let resu = await fn(item);
    return resu;
  });

  console.log(resuPromises);
  console.log("End");
}
fn1();

map.webp

如果你在 map 中使用 awaitmap 总是返回promises,你必须等待promises 数组得到处理。 或者通过await Promise.all()来完成此操作。

js
// 异步
let fn = (arg) => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(arg);
    }, 1000);
  });
};

let arr = [27, 0, 14];

async function fn1() {
  console.log("Start");

  let resuPromises = arr.map(async (item) => {
    let resu = await fn(item);
    return resu;
  });

  let res = await Promise.all(resuPromises);

  console.log(res);

  console.log("End");
}

fn1();

map2.webp

4. 在 filter 循环中使用 await

当你使用filter时,希望筛选具有特定结果的数组。假设过滤数量大于20的数组。

如果正常使用filter (没有 异步),如下:

js
let arr = [27, 0, 14];

function fn1() {
  console.log("Start");

  let resuPromises = arr.filter((item) => item > 20);

  console.log(resuPromises);

  console.log("End");
}

fn1();

// 输出:
Start
[ 27 ]
End

但是,若存在异步,filter 中的await不会以相同的方式工作。 事实上,它根本不起作用。

错误示例:

js
// 异步
let fn = (arg) => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(arg);
    }, 1000);
  });
};

let arr = [27, 0, 14];

function fn1() {
  console.log("Start");

  let resuPromises = arr.filter(async (item) => {
      let res = await fn(item)
      return res > 20
  });

  console.log(resuPromises);

  console.log("End");
}

fn1();

// 输出:
Start
[ 27, 0, 14 ]
End

当在filter 回调中使用await时,回调总是一个promise。由于promise 总是真的,数组中的所有项都通过filter 。在filter 使用 await类以下这段代码

js
const filtered = arr.filter(true);

filter使用 await 正确的三个步骤

  1. 使用map返回一个promise 结果数组
  2. 使用 await + Promise.all 取得处理结果
  3. 使用 filter 对返回的结果进行处理

5. 在 reduce 循环中使用 await

正常情况下,reduce的使用如下

js
let arr = [27, 0, 14];
function fn1() {
  console.log("Start");
  let count = arr.reduce((pre, cur) => {
    return (pre += cur);
  }, 0);
  console.log(count);

  console.log("End");
}
fn1();

// 输出
Start
41
End

但是,当reduce里面有异步操作,使用await会让结果出乎意料。

js
// 异步
let fn = (arg) => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(arg);
    }, 1000);
  });
};

let arr = [27, 0, 14];

async function fn1() {

  console.log("Start");

  let count =  await arr.reduce(async (pre, cur) => {
    let sum = await fn(cur);
    pre = pre + sum;
    return pre
  }, 0);

  console.log(count);
  console.log("End");
}
fn1();

注意:reduce有返回值,若reduce内部是异步函数,异步函数的返回值是promise,要取到promise的值,需要在reduce前也加上await,或通过then来取值。

reduce.webp

可以看到返回的值是[object Promise]14,来分析一下原因,来打印一下内部presum的值看。 即,在reduce里面加上一行console.log : console.log("sum--->:", sum, "pre--->:", pre);

js
let arr = [27, 0, 14];
async function fn1() {
  console.log("Start");
  let count = await arr.reduce(async (pre, cur) => {
    let sum = await fn(cur);
    console.log("sum--->:", sum, "pre--->:", pre);
    pre = pre + sum;
    return pre;
  }, 0);
  console.log(count);

  console.log("End");
}
fn1();

// 输出
Start
sum--->: 27  pre--->:  0
sum--->:  0   pre--->: Promise { 27 }
sum--->: 14  pre--->:  Promise { '[object Promise]0' }
[object Promise]14
End

原因一目了然。改造一下。等待一下pre,等curpre返回后再累计加。

js
let arr = [27, 0, 14];
async function fn1() {
  console.log("Start");
  let count = await arr.reduce(async (pre, cur) => {
    let p = await pre;
    let sum = await fn(cur);
    return p + sum;
  }, 0);
  console.log(count);

  console.log("End");
}
fn1();

输出结果: reduce2.webp

但是,可以看到reduce计算的时间有段长,因为每一次计算都需要先等待pre返回,时间累加起来就会很慢。

换种思路,先等待每个获取当前项值的fn异步函数返回,因为此时相当于并发操作,再等待pre,此时等待pre其实只是从padding中的promise里面获取已有的结果,等待时间可以忽略,因此并发的时间可以看成一个fn函数的返回时间,也就是一秒。

正确示例:

js
// 异步
let fn = (arg) => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(arg);
    }, 1000);
  });
};

let arr = [27, 0, 14];
async function fn1() {
  console.log("Start");
  let count = await arr.reduce(async (pre, cur) => {
    let sum = await fn(cur); // 先等待cur
    let p = await pre; // 再等待pre
    return p + sum;
  }, 0);
  console.log(count);

  console.log("End");
}
fn1();

reduce3.webp

因此,在reduce里面使用await时候,等待的顺序必须注意

在reduce中使用wait最简单(也是最有效)的方法是

  1. 使用map返回一个promise 数组
  2. 使用 await promise.all 等待处理结果
  3. 使用 reduce 对返回的结果进行处理(同步)

总结

  1. 如果你想连续执行await调用,请使用for循环(或任何没有回调的循环)。
  2. 永远不要和forEach一起使用await,而是使用for循环(或任何没有回调的循环)。
  3. 不要在 filterreduce 中使用 await,如果需要,先用 map 进一步骤处理,然后在使用 filterreduce进行处理。

参考文章

如何在 JS 循环中正确使用 async 与 await