关于请求这件小事> 作者:江苏苏 ## 一、引言 在日常开发过程中,经常与服务器进行交互。但其实,看似简单的请求,也有一
作者:江苏苏
一、引言
在日常开发过程中,经常与服务器进行交互。但其实,看似简单的请求,也有一些可以思考的点。大家都知道,浏览器是有并发限制的,最大并发数量是6-8个。那么,引出我们的第一个问题,针对一些相似请求,怎么合并请求?
二、案例
1. 相似请求合并请求
1.1. 背景
最近在梳理登录功能的过程中,发现页面中会多次调用usePromission这个接口来判断当前登录人是否有某个功能的权限。但是,如果页面中有多个功能点需要判断权限,那我们就要多次调用usePromission,也就意味着我们要发起多个相似的请求。但实际上,在实际应用中,针对一些复杂的应用,功能权限点可能很多,再加上页面的一些本身的查询,我们就有可能会超出浏览器的并发限制。所以,针对需要判断是否有某项功能的权限点,我们需要合并请求。大致流程如下:
1.2. 实现:相似请求合并请求
- 模拟请求
function mockRequest(params) {
return new Promise((resolve) => {
setTimeout(() => {
resolve(
params.map((p) => {
return {
code: p,
isAllow: Math.random() > 0.5,
};
})
);
}, 1000);
});
}
- 其余代码
// 使用 Promise.resolve() 创建一个 promise 实例,我们用它将一个任务添加到微任务队列
let p = Promise.resolve();
let isFlushing = false;
let params = new Set();
function execute() {
if (isFlushing) return;
isFlushing = true;
p = p.then(() => {
return mockRequest([...params]);
});
}
function print(param) {
params.add(param);关于请求这件小事
execute();
return p.then((data) => {
console.loh('data', data);
return data.find((item) => item.code === param).isAllow;
});
}
function test() {
for (let i = 0; i < 10; i++) {
print(i).then((isAllow) => {
console.log(isAllow);
});
}
}
test();
执行顺序如下:
- i = 0,执行print方法,将参数塞入全局的params中;执行execute方法,设置全局的isFlushing为true,同时,将promise pending赋值给p。继续往下执行
- i = 1,执行print方法,将参数塞入全局的params中;执行execute方法,此时isFlushing为true,直接返回。继续往下执行
- i = 2,3,4,5,6,7,8,9 同i = 1
- i = 10,i < 10,循环结束。开始执行微任务 p.then 并将结果返回。此时 17行的data打印的结果如下
2. 不同请求,展示最新的结果
2.1. 背景
在业务开发中,也曾遇到过,列表页通过tab区分,调用不同的接口展示不同的数据。在快速切换的情况下,期望展示的数据是最后一次请求的数据,但事实上,接口请求的时间是不确定的,所以,也会存在在点完未完成,立刻马上点已完成,如果此时,已完成的数据返回的比未完成的更快,那其实页面中展示的数据就是未完成的,但tab的高亮又是已完成的。就会造成数据不对。当然,解决这个问题的方式有很多种,那如果从请求入手,怎么实现不同请求,展示最新的结果呢?
2.2. 实现:不同请求,展示最新的结果
2.3. 例子
模拟:页面中存在两个按钮(点击获取3条数据、点击获取6条数据),我们先点获取6条数据,然后再快速点获取3条数据,通过模拟数据,设置获取6条数据更慢,然后发现,页面展示的是6条数据。按理说应该展示我最新获取的数据三条数据。
import { Button } from "antd";
import { useRef, useState } from "react";
const ClickCount = () => {
const [data, setData] = useState([]);
const fetch = (num) => {
// 模拟接口返回,为了明显看到效果,在这获取6条数据比获取3条数据慢
return new Promise((resolve) => {
setTimeout(() => {
resolve(Array(num).fill({}).map((item, index) => {
return {
index
}
}))
}, num * 500);
})
}
const getData = (num) => {
fetch(num).then((data) => {
setData(data);
})
}
return (
<div>
<Button onClick={() => {getData(3)}}>点击获取3条数据</Button>
<Button onClick={() => {getData(6)}}>点击获取6条数据</Button>
{
data.map(({ index }) => {
return <div key={index}>渲染的数据{index + 1}</div>
})
}
</div>
);
};
export default ClickCount;
2.4. 解决方案
只有当当前请求是最新请求时,结果才返回,否则结果就不返回。那怎么知道我当前的返回是最新一次的返回呢?
可以通过useRef。具体如下:
1.全局存储当前请求次数 const requestPool = useRef(0);
2.每次请求+1:
- requestPool.current += 1; // 每次请求加1
- const requestId = requestPool.current; // 记录当前是第几次请求
3.请求完成后,比较当前是否时最新返回:
- requestId === requestPool.current
import { Button } from "antd";
import { useRef, useState } from "react";
const ClickCount = () => {
const [data, setData] = useState([]);
const requestPool = useRef(0); // 记录最新请求
const fetch = (num) => {
return new Promise((resolve) => {
setTimeout(() => {
resolve(Array(num).fill({}).map((item, index) => {
return {
index
}
}))
}, num * 500);
})
}
const getData = (num) => {
requestPool.current += 1; // 每次请求加1
const requestId = requestPool.current; // 记录当前是第几次请求
fetch(num).then((data) => {
console.log('data', data)
if (requestId === requestPool.current) { // 只有当当前请求时最新请求时,才返回
setData(data);
}
})
}
return (
<div>
<Button onClick={() => {getData(3)}}>点击返回3条数据</Button>
<Button onClick={() => {getData(6)}}>点击返回6条数据</Button>
{
data.map(({ index }) => {
return <div key={index}>渲染的数据{index + 1}</div>
})
}
</div>
);
};
export default ClickCount;
页面效果
就算返回6条数据更慢,但页面只会展示最后一次请求获取的数据。
3. 不同请求,并发请求
3.1. 背景
尽管业务中不常见,但也可以考虑一下不同请求的并发的情况。例如:页面复杂,初始时加载多个接口,再例如下载中心下载多个不同的文件等,都需要考虑浏览器的并发限制。
3.2. 实现:不同请求,并发
3.3. 例子
模拟:页面中存在一个按钮(测试),点击触发6个模拟请求,限制同时并发2个。
import { Button } from "antd"
const Demo = () => {
// 模拟请求
const fetch = (num) => {
return new Promise((resolve) => {
setTimeout(() => {
console.log('num', num);
resolve(num)
// 注意:此处为了模拟接口的快慢,num为1、3、5时接口返回为1s,2、4、6 时接口返回是3s
}, 1000 + (num % 2 === 0 ? 2000 : 0));
})
}
const task1 = () => {
return fetch(1);
}
const task2 = () => {
return fetch(2)
}
const task3 = () => {
return fetch(3);
}
const task4 = () => {
return fetch(4)
}
const task5 = () => {
return fetch(5);
}
const task6 = () => {
return fetch(6)
}
// 一次点击,触发6个请求
const test = async () => {
task1();
task2();
task3();
task4();
task5();
task6();
}
return (
<Button onClick={test}>测试</Button>
)
}
export default Demo;
此时,执行顺序为1s后打印1,3,5。再等2s后打印2、4、6
那怎么实现并发请求数量为2的请求呢?
3.4. 解决方案
每次最多并发2个请求,请求完成了一个,那就继续发起下一个请求。基于此:
1.定义以下几个参数:
- activeCount 当前正在请求的个数
- result 并发返回的结果。这里需要注意,接口的返回是有快慢的,所以需要保证接口的返回跟请求的先后对应的上哦
2.初始,当前正在请求的个数小于默认并发的最大个数,发起请求;
3.发起请求后,正在请求的个数+1,只要请求完成了,请求的个数就-1:
- 判断是否有下一个请求,存在下个请求,继续发起请求
- 判断请求是否全都结束,全部结束,将结果返回
代码如下:
import { Button } from "antd"
const Demo = () => {
const fetch = (num) => {
return new Promise((resolve) => {
setTimeout(() => {
resolve(num)
}, 1000 + (num % 2 === 0 ? 2000 : 0));
})
}
// concurrency 默认并发的最大个数
const requestQueue = (concurrency) => {
return (urls) => {
let result = []; // 存储所有请求的结果
let curIndex = 0; // 请求的下标
let activeCount = 0; // 当前活跃请求的计数
return new Promise((resolve, reject) => {
const dequeue = (url, index) => {
activeCount++;
url().then((data) => {
result[index] = data;
}).finally(() => {
activeCount--
if (curIndex < urls.length) {
// url未请求完,继续下一个请求
dequeue(urls[curIndex], curIndex++)
}
if (activeCount === 0) {
// 无获取的请求,表明所有请求都已结束,返回结果
resolve(result);
}
})
}
while (curIndex < urls.length && activeCount < concurrency) {
dequeue(urls[curIndex], curIndex);
}
})
}
}
const enqueue = requestQueue(2);
const task1 = () => {
return fetch(1);
}
const task2 = () => {
return fetch(2)
}
const task3 = () => {
return fetch(3);
}
const task4 = () => {
return fetch(4)
}
const task5 = () => {
return fetch(5);
}
const task6 = () => {
return fetch(6)
}
const test = async () => {
const data = await enqueue([task1, task2, task3, task4, task5, task6])
console.log('data', data);
}
return (
<Button onClick={test}>测试</Button>
)
}
export default Demo;
此时,执行顺序是每隔1s分别出现 1、3、2、5、4。最后等待2s出现6。然后打印最终的返回结果【1、2、3、4、5、6】
不重要的解析:
- 初始,同时发起task1 & task2,task1的返回时间是1s,task2的返回时间3s
- 等待1s,task1执行完之后,打印出1;task3开始执行,此时task2还剩2s,task3的返回时间1s
- 等待1s,task3执行完之后,打印出3;task4开始执行,此时task2还剩1s,task4返回时间3s
- 等待1s,task2执行完之后,打印出2;task5开始执行,此时task4还剩2s,task5返回时间1s
- 等待1s,task5执行完之后,打印出5;task6开始执行,此时task4还剩1s,task6返回时间3s
- 等待1s,task4执行完之后,打印出4;
- 等待2s,task6执行完之后,打印出6;
三、总结
关于请求这件事,一个很小的切入点,但仔细想想,也是有一些值得思考的点存在。相似请求合并请求,不同请求并发请求,不同请求展示最新结果等等等。关于请求,当然远不止于这些。还有很多,但由于篇幅(我累了)原因,就到这吧。最后一句话:知识点无处不在,勤于思考,勤能补拙,天道酬勤。共勉~
转载自:https://juejin.cn/post/7406547615091802122