JavaScript异步循环-等待AJAX返回后再请求

最近遇到了一个需求就是需要循环发送Ajax请求,但是后一个请求必须等到前一个请求的返回后再发起。

这个问题可以使用递归解决,只需要在ajax的回调函数中调用自身就行了,当然本文中不使用这个办法(实际上嫌麻烦)。当时的第一个想法是使用promise,只需要在then中继续循环就ok,由于自身水平有限只能上网查询了一下,下面是网络中提供的代码:

var arr = [1, 2, 3, 4, 5, 6, 7, 8, 9]

function PromiseForEach(arr, cb) {
  let realResult = []
  let result = Promise.resolve()
  arr.forEach((a, index) => {
    result = result.then(() => {
      return cb(a).then((res) => {
        realResult.push(res)
      })
    })
  })
  return result.then(() => {
    return realResult
  })
}

PromiseForEach(arr, (ele) => {

  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log(ele);
      return resolve(ele);
    }, 1000);
  })
}).then((data) => {
  console.log("成功");
  console.log(data);
}).catch((err) => {
  console.log("失败");
  console.log(err)
});

读这段代码还是花了一些功夫,针对promise我只是用过,很多细节不清楚。但是这段代码只需要知道Promise.resolve()的结果和then的的返回值就可以读懂。我来分析一下这段代码。首先运行一遍,得知运行结果为(其中数字之间间隔1s打印):


1
2
3
4
5
6
7
8
9
成功
Array(9) [1, 2, 3, 4, 5, 6, 7, 8, …]

我这这么阅读这段代码的:

  1. 忽略函数定义,从运行阶段开始看起
PromiseForEach(arr, (ele) => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log(ele);
      return resolve(ele);
    }, 1000);
  })
}).then((data) => {
  console.log("成功");
  console.log(data);
}).catch((err) => {
  console.log("失败");
  console.log(err)
});

这里调用了PromiseForEach函数。从写法看出传入了两个参数,第一个为数组,第二个为回调函数,函数后跟着then方法说明PromiseForEach函数的返回值是一个promise,当此promise的状态为fulfilled时调用此方法。

  1. 开始查看PromiseForEach函数的定义
function PromiseForEach(arr, cb) {
  let realResult = []
  let result = Promise.resolve()
  arr.forEach((a, index) => {
    result = result.then(() => {
      return cb(a).then((res) => {
        realResult.push(res)
      })
    })
  })
  return result.then(() => {
    return realResult
  })
}

首先查看函数的返回值,寻找什么时机函数进行返回。此时返回的是result.then(() => { return realResult }),而result的定义又是Promise.resolve(),此时查阅文档得知Promise.resolve()的返回值为一个promise且它的状态为fulfilled,而then的返回值有些复杂( 这里的then返回值满足最后一条,是一个pending状态的promise):

  • 返回了一个值,那么 then 返回的 Promise 将会成为接受状态,并且将返回的值作为接受状态的回调函数的参数值。
  • 没有返回任何值,那么 then 返回的 Promise 将会成为接受状态,并且该接受状态的回调函数的参数值为 undefined。
  • 抛出一个错误,那么 then 返回的 Promise 将会成为拒绝状态,并且将抛出的错误作为拒绝状态的回调函数的参数值。
  • 返回一个已经是接受状态的 Promise,那么 then 返回的 Promise 也会成为接受状态,并且将那个 Promise 的接受状态的回调函数的参数值作为该被返回的Promise的接受状态回调函数的参数值。
  • 返回一个已经是拒绝状态的 Promise,那么 then 返回的 Promise 也会成为拒绝状态,并且将那个 Promise 的拒绝状态的回调函数的参数值作为该被返回的Promise的拒绝状态回调函数的参数值。
  • 返回一个未定状态(pending)的 Promise,那么 then 返回 Promise 的状态也是未定的,并且它的终态与那个 Promise 的终态相同;同时,它变为终态时调用的回调函数参数与那个 Promise 变为终态时的回调函数的参数是相同的。
  1. 查看promise的执行和返回时机

首先函数创建了一个result变量,因为它的状态是fulfilled,所以它的then会立即执行,把执行后的结果又赋值给了result。那么这个结果是什么呢,查看上面then返回值得知满足最后一条。
现在可以查看cb回调函数在什么时机会变成fulfilled, 查看得知在1s后会执行cb的then,then执行完成后此时返回值满足了上面的第一条,状态从pending变为fulfilled,把这个结果传递到result上面。
此时PromiseForEach函数还不会开始返回,因为这个result虽然变成了fulfilled,但是此时的result已经不是最初的result了,foreach进行了9此循环,创建了9个promise对象。 函数返回的是最后一个,刚才执行的只是第一个。因为result一直是在被覆盖,前一个的promise变成了fulfilled后后一个promise的then才会执行,而前一个的promise状态会在1s后才变化,所以此时达到了异步遍历的效果。

刚才方法的重点是把promise的状态给串起来,一句话概括这就是链式写法的变种


既然这样那不如使用async/await,来的更方便,我对上面代码进行了修改:

var arr = [1, 2, 3, 4, 5, 6, 7, 8, 9]

async function PromiseForEach(arr, cb) {
  let realResult = []
  for (let item of arr) {
    await cb(item)
    realResult.push(item)
  }
  return new Promise((reslove, reject) => {
    reslove(realResult)
  })
}

PromiseForEach(arr, (ele) => {

  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log(ele);
      return resolve(ele);
    }, 1000);
  })
}).then((data) => {
  console.log("成功");
  console.log(data);
}).catch((err) => {
  console.log("失败");
  console.log(err)
});

效果和上面代码相同,但是好理解,代码量也更少。


2019年11月25更新: 才发现for...of...使用的异步迭代是ES2018的内容.async/await是ES2017的内容,for...of...是ES2015的内容,如果在ES2018之前的环境执行上面的代码是无法正确执行的.

2 评论

  1. 姜辰 2019年11月21日 回复
    大佬电脑是mac还是ubuntu?
    1. Curtion [博主] 2019年11月21日 回复
      回复 姜辰: 都不是,win + deepin. 哭了,买不起Mac