最近在总结异步的一些实现方式,也是翻出了一道比较经典的 promise 面试题与大家分享。
当然具体实现代码比较长,所以面试只是问了思路。今天找时间把它实现出来。
进入正题:
现假设后端有一个服务,用于批量获取书籍信息,接受一个数组作为请求参数,数组储存了需要获取书籍信息的书籍 id,
这个服务 fetchBooksInfo 大概是这个样子:
const fetchBooksInfo = bookIdList => {
const data = [
{
id: 123
// ...
},
{
id: 456
// ...
},
{
id: 789
// ...
}
// ...
];
// 模拟查找数据逻辑 并返回promise对象
let result = data.filter(item => bookIdList.indexOf(item.id) > -1);
return Promise.resolve(result);
};
需求
现有如下要求:
- fetchBooksInfo 已经给出,但是这个接口单次只支持最多 100 个 id 的查询。
- 现需要实现 getBooksInfo 方法,该方法的要求为:
2-1. 支持调用单个书目信息,如:
getBooksInfo(123).then(data => { console.log(data.id) }) // 123
2-2. 短时间(100 毫秒)内多次连续调用,只请求一次 fetchBooksInfo 服务,且获得各个书目信息,如:
getBooksInfo(123).then(data => { console.log(data.id) }) // 123
getBooksInfo(456).then(data => { console.log(data.id) }) // 456 - 要考虑服务端出错的情况,比如批量接口请求[123, 446] 书目信息,但是服务端只返回了书目123的信息。此时应该进行合理的错误处理。
- 对 ID 重复进行处理
思路
认真读完要求后,来分析下题目,梳理思路:
- 首先要对 100 毫秒内的连续请求进行合并,只触发一次请求。考虑可以使用数组存储多条请求的id,并使用定时器处理 100 毫秒的延迟。
- 请求的数据长度达到 100 时,清除定时器,直接发送请求。
- 还要通过返回的数据判断是否包含全部请求参数中的数据,如果缺失需要抛出错误提示。
- 使用 promise 毋庸置疑,我觉得这道题最重要的考点就在于如何在合适的时机进行 reject 或 resolve 对应的 promise 实例。我的做法是对每一个 getBooksInfo 对应的 promise 实例的 reject 和 resolve 方法进行存储。以便于可以在需要的时候进行触发。
实现
完整的代码实现:
// 题目给出的服务代码
const fetchBooksInfo = bookIdList => {
const data = [
{
id: 123
// ...
},
{
id: 456
// ...
},
{
id: 789
// ...
}
// ...
];
// 模拟查找数据逻辑 并返回promise对象
let result = data.filter(item => bookIdList.indexOf(item.id) > -1);
return Promise.resolve(result);
};
// 以下为我的代码实现↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
// 储存将要请求的 id 数组
let bookIdListToFetch = [];
// 用于储存每个 id 请求 promise 实例的 resolve 和 reject
// key 为 bookId,value 为 resolve 和 reject 方法
// 如: { 123: [{resolve, reject}]}
let promiseMap = {};
// 数组去重的方法
const getUniqueArray = array => Array.from(new Set(array));
// 定时器 id
let timer;
// getBooksInfo 方法
const getBooksInfo = bookId =>
new Promise((resolve, reject) => {
promiseMap[bookId] = promiseMap[bookId] || [];
// 保存每个 id 请求 promise 实例的 resolve 和 reject
promiseMap[bookId] = {
resolve,
reject
};
// 清空数据
const clearTask = () => {
bookIdListToFetch = [];
promiseMap = {};
};
// 如果数组为空
if (bookIdListToFetch.length === 0) {
bookIdListToFetch.push(bookId);
// 定时100ms 到时自动执行
timer = setTimeout(() => {
// 调用服务获取数据 && 清空存储
handleFetch(bookIdListToFetch, promiseMap);
clearTask();
}, 100);
} else {
// 如果数组已有数据
// 添加进数组
bookIdListToFetch.push(bookId);
// 去重
bookIdListToFetch = getUniqueArray(bookIdListToFetch);
// 数量大于100
if (bookIdListToFetch.length >= 100) {
// 清除计时器 && 调用服务获取数据 && 清空存储
clearTimeout(timer);
handleFetch(bookIdListToFetch, promiseMap);
clearTask();
}
}
});
// 调用服务获取数据
const handleFetch = (list, map) => {
// 获取图书信息
fetchBooksInfo(list).then(resultArray => {
console.log("调用服务");
// 保存获取的图书 bookId
const resultIdArray = resultArray.map(item => item.id);
// 处理存在的 bookId
resultArray.forEach(data => map[data.id].resolve(data));
// 处理失败没拿到的 bookId
let rejectIdArray = [];
list.forEach(id => {
// 返回的数组中,不含有某项 bookId,表示请求失败
if (!resultIdArray.includes(id)) {
rejectIdArray.push(id);
}
});
// 对请求失败的数组进行 reject
rejectIdArray.forEach(id => map[id].reject(id));
});
};
至此所有代码已经完成,接下来验证 getBooksInfo 方法是否满足需求。
验证1
getBooksInfo(123).then(data => {
console.log(data.id); // 123
});
getBooksInfo(456).then(data => {
console.log(data.id); // 456
});
执行结果:
两次调用 getBooksInfo 方法,均返回数据,并且是只调用了一次 fetchBooksInfo 服务,满足需求。
验证2
模拟一次失败的获取,因为“服务端”没有446这条数据,返回的数据必然缺少一条.
getBooksInfo(446)
.then(data => {
console.log(data.id);
})
.catch(data => {
console.log(`获取数据${data}出错了`); // 获取数据 446 出错了
});
执行结果:
程序正常抛出错误提示。