最近遇到了一个需求就是需要循环发送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, …]
我这这么阅读这段代码的:
- 忽略函数定义,从运行阶段开始看起
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
时调用此方法。
- 开始查看
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 变为终态时的回调函数的参数是相同的。
- 查看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 评论