如何实现按顺序返回异步请求
如何实现按顺序返回异步请求
最近我在面试字节飞书出了一道手写题,是一道顺序返回异步请求的题,当时情况我采用了 async 与 await 和 Promise.all 两种方法来进行实现,事后想来这两种都不完全符合题意,我搜索网上的文章都没有符合我心意的答案,于是自己便写出来总结一下
一、题干
function test(i){
return new Promise((res)=>{
setTimeout(()=>{
res(i)
},Math.random()*1000)
})
}
function main(){
for(let i=0;i<10;i++){
test(i).then((res)=>{
console.log(res)
})
}
}
main()
要求:要求按顺序打印 0 到 9。
实质:该问题实质是模拟多个异步请求,并确保这些请求的结果按顺序返回和输出
二、采用 async 和 await
分析: 首先我们很容易就想到了采用 async 和 await 来实现,通过对每一个请求的等待,在它完成响应后再进行下一个请求的发送,这样就实现了结果按照 0 到 9 顺序打印
代码实现:
//通过添加async和await实现
async function main(){
for(let i=0;i<10;i++){
await test(i).then((res)=>{
console.log(res)
})
}
}
main()
结论:这样我们就实现了题目要求,但是我们仔细分析就知道,它这其实是按顺序进行请求,而不是按顺序接受响应结果,这样每个请求都得等待前一个请求完成,大大降低了时间效率,所以这种方法其实并不可取
三、采用 Promise.all
分析: 分析 Promise.all 的源码实现我们知道,Promise.all 的内部实现是维护一个 arr 数组,每个请求发送后,通过发送时的 index 序号,将它的结果保存 arr[index] 中,在所有请求都 resovle 之后,统一返回一个 Promise.resolve 来接受这个 arr 值 ( 这里只考虑所有请求都成功 ),所以,使用在这道题目上也适合
代码实现:
function main(){
let arr=[]
let key=0
const promise=new Promise((resovle)=>{
for(let i=0;i<10;i++){
test(i).then((result)=>{
arr[i]=result
key++
//当所有的值都返回后,统一resolve返回arr结果
if(key===10){
resovle(arr)
}
})
}
})
//遍历数组,打印结果
promise.then((res)=>{
res.forEach(item => {
console.log(item)
});
})
}
main()
结论:如此我们也实现了按顺序打印的要求,但是这个是在所有请求都响应完毕后,再统一遍历来打印出来,这并不满足 返回了结果符合的,就立即响应的需求,所以只能作为备选项
四、指针锁调度
分析: 根据题意,它想要的是根据顺序来打印结果,但是由于返回的时间不同,导致后面的数据可能先返回,而前面的数据后返回的问题,那么其实我们可以根据一个指针 key,来维护异步调度的数组执行。每当有值返回我们给它存在 arr 数组中,根据 key 和 index 的对比,来判断是否打印值和后续值
代码实现:
function main(){
//指针key
let key = 0
const arr=[]
for(let i=0;i<10;i++){
test(i).then((res)=>{
//返回结果后就存进arr数组中
arr[res]=()=>console.log(res)
//判断指针key的指向以及它后续指向来进行函数执行打印
if(key===res){
while(arr[key]){
arr[key]()
key++
}
}
})
}
}
main()
结论: 这种方法可以被称为 “有序回调执行” 或者“基于索引的有序输出”。它通过维护一个数组来存储回调函数,并使用一个指针来确保按顺序执行这些回调函数,即使这些回调函数是异步完成的。该方法的核心思想是延迟执行,直到所有前面的回调都准备好,从而保证输出的顺序,完全符合题意。
五、优化
优化原因: 我们知道通过方法四指针锁调度可以完美实现题目要求,但是这里毕竟是模拟请求,既然是请求,就难免会遇到请求失败,错误处理的问题,所以,这里拓展模拟一下部分失败时,我们该怎么处理
代码实现:
function test(i) {
return new Promise((res, rej) => {
setTimeout(() => {
if (Math.random() > 0.8) { // 20% 的概率失败
rej(`Request ${i} failed`);
} else {
res(i);
}
}, Math.random() * 1000);
});
}
function main() {
let key = 0;
const arr = [];
for (let i = 0; i < 10; i++) {
test(i)
.then((res) => {
arr[res] = () => console.log(res, +new Date());
})
.catch((err) => {
arr[i] = () => console.log(`Request ${i} failed`);
})
.finally(() => {
// 检查并按顺序输出
if (key === i) {
while (arr[key]) {
arr[key]();
key++;
}
}
});
}
}
main();
结论:这样,一个完整逻辑的指针锁调度代码就实现啦!
六、总结
现在我们知道要实现题干要求,我们有三种方法可以实现,但指针锁调度无疑是最适合该题目的解决方法,这种思想,其实在很多场景都有使用,比如:
- 网络请求: 向多个 API 端点发送请求,并确保按特定顺序处理和显示结果。例如,获取多个用户的详细信息,并按用户 ID 顺序显示。
- 文件处理: 读取多个文件的内容并按顺序处理,而不管读取操作的完成顺序。例如,读取多个日志文件,并按时间顺序处理日志条目。
- 数据流处理: 在数据流处理中,确保数据块按顺序处理和输出。例如,处理视频流或音频流数据,确保帧按正确顺序播放。
- 任务调度: 在任务调度系统中,确保任务按特定顺序执行和完成。例如,处理一系列依赖关系的任务,每个任务完成后按顺序执行下一个任务。
但其他两种方法也有各种适合的场景,这里就不展开叙述了
这里是 Tommya,一位在求职的大四前端开发者,希望我的解答能给你带来一些帮助