我们经常会遇到一个场景,比如在一个列表中批量获取用户的信息。
如果我们一次性往后端发送几十条请求是非常愚蠢的事情。此时我们就要学会如何使用批量获取的逻辑。
但是批量获取有一个问题就是,我需要在用户列表项的上层去获取,然后再把结果分发给下层
此时的结构如下:
const List = () => {
return itemInfoList.map((info) => <Item info={info} />)
}
这样我们就可以很方便的解决我们遇到的问题啦,因为是一个接口获取的结果嘛。
但是!这种写法不利于维护。因为 <Item /> 组件的依赖过于庞大,是一个完整的对象。对于其他组件来说很难复用,大概率这个组件就只能在一处地方用了。那么我想要复用怎么办呢?
那就是写成如下形式
const List = () => {
return ids.map((id) => <Item id={id} />)
}
然后 <Item /> 组件内部就可以根据传入的id自行获取对应的数据,然后自我处理了。
那么不就遇到开头这个问题了嘛?如果一次渲染几十个用户组件,那么不就同时向后端发送几十个网络请求了嘛!
这时候我们需要实现一个逻辑,自动收集并合并可以被合并的网络请求。
完整代码如下:
interface QueueItem<T, R> {
params: T;
resolve: (r: R) => void;
reject: (reason: unknown) => void;
}
/**
* 创建一个自动合并请求的函数
* 在一定窗口期内的所有请求都会被合并提交合并发送
* @param fn 合并后的请求函数
* @param windowMs 窗口期
*/
export function createAutoMergedRequest<T, R>(
fn: (mergedParams: T[]) => Promise<R[]>,
windowMs = 200
): (params: T) => Promise<R> {
let queue: QueueItem<T, R>[] = [];
let timer: number | null = null;
async function submitQueue() {
timer = null; // 清空计时器以接受后续请求
const _queue = [...queue];
queue = []; // 清空队列
try {
const list = awAIt fn(_queue.map((q) => q.params));
_queue.forEach((q1, i) => {
q1.resolve(list[i]);
});
} catch (err) {
_queue.forEach((q2) => {
q2.reject(err);
});
}
}
return (params: T): Promise<R> => {
if (!timer) {
// 如果没有开始窗口期,则创建
timer = window.setTimeout(() => {
submitQueue();
}, windowMs);
}
return new Promise<R>((resolve, reject) => {
queue.push({
params,
resolve,
reject,
});
});
};
}
用法是:
const fetchUserInfo = createAutoMergedRequest<string, UserBaseInfo>(
async (userIds) => {
const { data } = await request.post('/api/user/getUserInfoList', {
userIds,
});
return data;
}
);
fetchUserInfo(1)
fetchUserInfo(2)
fetchUserInfo(3)
接下来我们来解读一下代码。
先看整体架构。 createAutoMergedRequest 函数返回了一个匿名函数,来接受参数并返回结果请求。但是需要注意的是我们定义了两个泛型 T 和 R 。其中 createAutoMergedRequest 接受的 fn 参数的类型是 (mergedParams: T[]) => Promise<R[]> ,而返回的函数定义是 (params: T): Promise<R> 。这是因为他会自动把请求的结果拆分成独立的返回值返回到对应的调用处。
我们看返回的函数体:
if (!timer) {
// 如果没有开始窗口期,则创建
timer = window.setTimeout(() => {
submitQueue();
}, windowMs);
}
return new Promise<R>((resolve, reject) => {
queue.push({
params,
resolve,
reject,
});
});
首先判断闭包中是否存在定时器 timer, 如果没有则创建一个timer,在 windowMs 后执行 submitQueue 方法。我们把 windowMs 定义为窗口期,在这个窗口期内调用该函数的请求都会被收集起来。
然后创建返回一个promise,把参数和promise相关的下一步操作都推到 queue 中。
等到若干次调用后,定时器到时间了,唤起回调执行submitQueue 方法,我们来看看 submitQueue 的操作。
async function submitQueue() {
timer = null; // 清空计时器以接受后续请求
const _queue = [...queue];
queue = []; // 清空队列
const ret = fn(_queue.map((q) => q.params));
try {
const list = await fn(_queue.map((q) => q.params));
_queue.forEach((q1, i) => {
q1.resolve(list[i]);
});
} catch (err) {
_queue.forEach((q2) => {
q2.reject(err);
});
}
}
执行前我们会做一些前置工作,清理 timer, 清理 queue 并把队列里的项单独存放起来,防止影响到下一次执行。
然后我们通过 fn(_queue.map((q) => q.params)) 来把队列中的参数拿出来,传给 fn 调用。此时的 fn 就会接收到一个数组。并确保返回的结果也是一个同等大小且一一对应的数据即可。如果请求无误,我们就循环队列,把结果通过队列中记录的 resolve 把结果返回给我们之前创建的promise。
这样我们就实现了一个工具函数,我们可以在一个窗口期内收集到多个网络请求,并把他们汇聚成一个请求发送到后端。后端结果返回回来后,我们再把请求结果拆分分发给独立的调用方。
本文实战用法见:
tailchat/client/shared/utils/request.ts at master · msgbyte/tailchat · Github
我会不定期的从 Tailchat 的源码中拆出一些实用的实战小技巧分享给大家,关注我,并给 Tailchat 点点 star。我将带你以实战理解算法的妙用。
Tailchat - The next-generation noIM Application in your own workspace | Tailchat