大数据表单元格批量请求优化方案
背景
在表格组件中,每个单元格是一个vue实例,数据请求、数据处理、数据展示都由组件内部管理,虽然原则上实现了数据的解耦,但是也引入了一个问题,即数据的请求次数与单元格的数量呈正相关,导致在大量单元格渲染的场景下,请求发送特别频繁
优化思路
仔细观察表格内的场景以及单元格的请求格式,会发现两个特点:
- 同一列的单元格请求的数据接口是一样的,参数类似
- 数据接口往往支持批量处理
既然如此,是否可以将一定时间段内的请求参数合并去重后再提交,以达到减少请求的目的
方案
请求队列
此方案适合参数根据单元格的具体内容填入请求参数的场景
方案流程:
- 各单元格将要请求的数据推送到批量请求模块中,通过回调函数等待回调数据
- 请求队列通过一个worker中的定时触发器来控制消费速度
- 调度信号来了以后,将当前请求数据合并去重,然后发起请求
- 请求成功后将数据抛给回调函数,触发组件更新
为什么要把队列触发器放在worker?
大数据场景下,单元格渲染很慢。如使用setTimeout防抖会导致发送请求的时间推迟到所有单元格渲染完成,出现长时间白屏;如使用setInterval节流,主线程被渲染卡住导致setInterval一直被延后到所有单元格渲染完成,也会出现长时间白屏。
代码示例
const MAX_REQUEST_COUNT = 100;
const queue = {
codes: [],
callbacks: []
}
// 外部调用
const batchGetter = (
codes: string[],
callback: (data: any[]) => void,
) => {
queue.codes = queue.codes.concat(codes);
queue.callbacks.push(callback);
worker.postMessage({
method: 'start',
id: 'batchGetter',
interval: 100,
});
};
// 发起请求
const startRequest = async () => {
const len = queue.codes.length;
if (!len) {
worker.postMessage({
method: 'stop',
id: 'batchGetter',
});
return;
}
const valueSet = new Set(queue.codes);
const fns = queue.callbacks.slice(0, len);
queue.codes.splice(0, len);
queue.callbacks.splice(0, len);
if (valueSet.size) {
let res: any = [];
try {
const response = await Promise.allSettled(
chunk([...valueSet], MAX_REQUEST_COUNT).map((codes) => {
return api({ codes });
}),
);
const data: any = [];
response.forEach((item) => {
if (item.status === 'fulfilled') {
data.push(...item.value);
}
});
res = data || [];
} catch (e) {
res = [];
}
for (const fn of fns) {
fn(res);
}
} else {
for (const fn of fns) {
fn([]);
}
}
};
// worker中的队列调度
const idMap: Record<string, any> = {};
addEventListener('message', (e: MessageEvent) => {
const { id, method, interval = 100 } = e.data;
if (method === 'start') {
if (!idMap[id]) {
idMap[id] = setInterval(() => {
postMessage({
id,
method: 'run',
});
}, interval);
}
} else if (method === 'stop') {
clearInterval(idMap[id]);
delete idMap[id];
}
});
// 主线程监听
worker.addEventListener('message', (e) => {
const { id, method } = e.data;
if (method === 'run') {
switch (id) {
case 'batchGetter':
startRequest();
break;
}
}
});
请求复用
该方案适合参数固定的请求,拉取内容保持一致,可缓存
方案流程
- 发起请求后,根据函数名 + 参数列表生成一个标记符
- 如果发现标记符能在数据缓存记录中找到,直接返回缓存的数据
- 如果发现标记符能在请求缓存记录中找到,返回缓存的请求
- 发起请求,将当前promise保存到请求缓存记录中,请求结束后将返回内容缓存到数据缓存记录中,并把promise从请求缓存记录中删掉
代码示例
const apiCache = new Map<string, Promise<any>>();
const resultCache = new Map<string, any>();
const lruQueue: string[] = [];
const LRU_MAX = 100;
const batchGetFullData = (
api: (params: any, silent: boolean) => Promise<any>,
params: AnalyserNode,
) => {
const key = `${api?.name || 'default'}_${JSON.stringify(params)}`;
if (resultCache.has(key)) {
lruQueue.splice(lruQueue.indexOf(key), 1);
lruQueue.unshift(key);
return Promise.resolve(resultCache.get(key));
}
if (apiCache.has(key)) {
return apiCache.get(key);
}
const promise = new Promise((resolve) => {
api(params, true)
.then((res) => {
lruQueue.splice(lruQueue.indexOf(key), 1);
lruQueue.unshift(key);
while (lruQueue.length > LRU_MAX) {
const removeKey = lruQueue.pop();
if (removeKey) {
resultCache.delete(removeKey);
}
}
resultCache.set(key, res);
apiCache.delete(key);
resolve(res);
})
.catch(() => {
lruQueue.splice(lruQueue.indexOf(key), 1);
apiCache.delete(key);
resultCache.delete(key);
resolve([]);
});
});
apiCache.set(key, promise);
return promise;
};