页面上存在很多获取下拉选项的请求,需要合并,减少请求次数。
接口情况:获取下拉选项是一个通用接口,根据不同的 code 返回对应的下拉选项。
为了支持批量功能,接口已改造为接受使用逗号连接的多个 code。
解决方式有2种,一种是修改业务代码,手动拼接 code。另一种则是改造 api 通过防抖批量查询,业务无感知,下面就介绍下这种方案。
方案
- 排队,接收待发送的请求,并添加订阅
- 批量查询,使用防抖
- 查询结果返回后批量通知
排队
调用 api 后,进入队列,并添加订阅。
// 待请求队列
let toFetch = [];
// 发布订阅队列
let resolves = [];
let rejects = [];
// 向外提供的获取下拉选项的api
export const getOptions = (code) => new Promise((resolve, reject) => {
// 消息订阅
resolves.push(resolve);
rejects.push(reject);
// 进入队列
toFetch.push(code);
});
批量查询
使用防抖进行批量查询
- 如果有新的请求,重置定时器;
- 如果一段时间内没有新的请求,开始批量查询并清空队列。
// 定时器
let timeout;
// 等待间隔
const interval = 100;
// 批量查询接口
const batchFetch = () => {
request.get(`${url}/${toFetch.join(',')}`) // TODO 修改url
// 清空队列
toFetch = [];
};
// 向外提供的获取下拉选项的api
export const getOptions = (code) => new Promise((resolve, reject) => {
...
/* 结尾添加防抖 */
// 如果有新的请求,重置定时器
clearTimeout(timeout);
timeout = setTimeout(() => {
// 批量查询
batchFetch();
}, interval);
})
通知
存储返回结果,通知各请求取结果。
// 下拉选项结果集合
let optionsMap = {}
// 批量查询接口
const batchFetch = () => {
// TODO 修改url
request.get(`${url}/${toFetch.join(',')}`)
.then((data) => {
// 存储结果
optionsMap = data;
// 发布成功通知
resolves.forEach((resolve) => resolve());
})
.catch(() => {
// 发布失败通知
rejects.forEach((reject) => reject());
})
.finally(() => {
// 清空订阅队列
resolves = [];
rejects = [];
});
// 清空队列
toFetch = [];
};
// 向外提供的获取下拉选项的接口
export const getOptions = (code) => new Promise((resolve, reject) => {
...
}).then(() => optionsMap[code]); // 根据code取结果
优化
code重复- 队列长度
- 结果缓存
code 重复
多个请求的 code 相同,不用重复排队。
待请求队列不重复排队
// 向外提供的获取下拉选项的接口
export const getOptions = (code) => new Promise((resolve, reject) => {
// 消息订阅
resolves.push(resolve);
rejects.push(reject);
/* ++++++++++++++ */
// 不重复排队
if (toFetch.includes(code)) {
return;
}
/* ++++++++++++++ */
...
}).then(() => optionsMap[code]); // 根据code取结果
请求中队列
这样还不够,在接口请求发出后,返回前,因为队列已被清空,此时 code 相同仍需排队。
因此添加请求中队列。
// 请求中队列
let fetching = [];
const batchFetch = () => {
/* ++++++++++++++ */
// 队列交接,清空排队队列
fetching = toFetch;
toFetch = [];
/* ++++++++++++++ */
// TODO 修改url
// `code` 拼接使用请求中队列
request.get(`${url}/${fetching.join(',')}`)
...
.finally(() => {
// 清空订阅队列
resolves = [];
rejects = [];
// 清空请求中队列
fetching = [];
})
};
// 向外提供的获取下拉选项的接口
export const getOptions = (code) => new Promise((resolve, reject) => {
// 消息订阅
resolves.push(resolve);
rejects.push(reject);
/* ---+++++++++++ */
// 不重复排队
if (fetching.includes(code) || toFetch.includes(code)) {
return;
}
/* ---+++++++++++ */
...
}).then(() => optionsMap[code]); // 根据code取结果
队列长度
批量请求 code 过多也会影响性能,因此添加长度限制。
可能存在多个批量请求同时返回的情况,因此存储结果不能直接替换,要改为合并了。
// 批量查询结果
const optionsMap = {}
// 队列最大长度
const maxLength = 10;
const batchFetch = () => {
...
// TODO 修改url
request.get(`${url}/${fetching.join(',')}`)
.then((data) => {
/* ---+++++++++++ */
// 存储结果只添加和覆盖,不再直接替换
Object.keys(data).forEach((key) => {
optionsMap[key] = data[key];
});
/* ---+++++++++++ */
// 发布成功通知
resolves.forEach((resolve) => resolve());
})
...
};
// 向外提供的获取下拉选项的接口
export const getOptions = (code) => new Promise((resolve, reject) => {
/* 修改防抖部分函数 */
/* ---+++++++++++ */
clearTimeout(timeout);
// 队列满直接发请求
if (toFetch.length >= maxLength) {
batchFetch();
} else {
timeout = setTimeout(() => {
batchFetch();
}, interval);
}
/* ---+++++++++++ */
...
}).then(() => optionsMap[code]); // 根据code取结果
结果缓存
经过上一步修改,下拉选项结果一直在内存中,有两处需要调整。
- 如果新的请求已有结果,可以直接使用。
- 需要适时释放内存。
export const getOptions = (code) => {
// 如果内存中有结果,直接使用
if (optionsMap[code]) {
return Promise.resolve(optionsMap[code]);
}
return new Promise(...).then(() => optionsMap[code]); // 根据code取结果
}
// 清空队列时释放内存
const batchFetch = () => {
...
request.get(`${url}/${fetching.join(',')}`)
...
.finally(() => {
// 清空订阅队列
resolves = [];
rejects = [];
// 释放内存
fetching.forEach((code) => {
delete optionsMap[code]
})
// 清空请求中队列
fetching = [];
})
};
最终代码
// 待请求队列
let toFetch = [];
// 请求中队列
let fetching = [];
// 订阅队列
let resolves = [];
let rejects = [];
// 定时器
let timeout;
// 定时器间隔
const interval = 100;
// 下拉选项结果
const optionsMap = {};
// 队列最大长度
const maxLength = 10;
// 批量查询
const batchFetch = () => {
// 队列交接,清空排队队列
fetching = toFetch;
toFetch = [];
// TODO 修改url
request.get(`${url}/${fetching.join(',')}`)
.then((data) => {
// 存储结果合并
Object.keys(data).forEach((key) => {
optionsMap[key] = data[key];
});
// 发布成功通知
resolves.forEach((resolve) => resolve());
})
.catch(() => {
// 发布失败通知
rejects.forEach((reject) => reject());
})
.finally(() => {
// 清空订阅队列
resolves = [];
rejects = [];
// 释放内存
fetching.forEach((code) => {
delete optionsMap[code]
})
// 清空请求中队列
fetching = [];
});
};
// 向外提供的获取下拉选项的接口
export const getOptions = (code) => {
// 如果已包含结果,直接使用
if (optionsMap[code]) {
return Promise.resolve(optionsMap[code]);
}
return new Promise((resolve, reject) => {
// 消息订阅
resolves.push(resolve);
rejects.push(reject);
// 不重复排队
if (fetching.includes(code) || toFetch.includes(code)) {
return;
}
// 进入队列
toFetch.push(code);
// 如果有新的请求,重置定时器
clearTimeout(timeout);
// 队列满直接发请求
if (toFetch.length >= maxLength) {
batchFetch();
} else {
// 设置定时器
timeout = setTimeout(() => {
batchFetch();
}, interval);
}
}).then(() => optionsMap[code]); // 根据code取结果
}