【经典面试题系列】实现满足条件的异步请求

今天给大家继续分享一篇经典的前端面试题,来自阿里核心部门P7的一次面试。

假设现在后端有一个服务,支持批量返回书籍信息,它接受一个数组作为请求数据,数组储存了需要获取书目信息的书目 id,这个服务 fetchBooksInfo 大概是这个样子:

const fetchBooksInfo = bookIdList => {
   // ...
   return ([{

           id123,
           // ...

       },
       {
           id456
           // ...
       },
       // ...
   ])
}

fetchBooksInfo 已经给出,但是这个接口最多只支持 100 个 id 的查询。

现在需要开发者实现 getBooksInfo 方法,该方法:

  • 支持调用单个书目信息:
getBooksInfo(123).then(data => {console.log(data.id)}) // 123
  • 短时间(100 毫秒)内多次连续调用,只发送一个请求,且获得各个书目信息:
getBooksInfo(123).then(data => {console.log(data.id)}) // 123
getBooksInfo(456).then(data => {console.log(data.id)}) // 456

*注意这里必须只发送一个请求,也就是说调用了一次 fetchBooksInfo。

  • 要考虑服务端出错的情况,比如批量接口请求 [123, 446] 书目信息,但是服务端只返回了书目 123 的信息。此时应该进行合理的错误处理。

  • 对 id 重复进行处理

题目分析

  • 100 毫秒内的连续请求,要求进行合并,只触发一次网络请求。因此需要一个 bookIdListToFetch 数组,并设置 100 毫秒的定时。在 100 毫秒以内,将所有的书目 id push 到 bookIdListToFetch 中,bookIdListToFetch 长度为 100 时,进行 clearTimeout,并调用 fetchBooksInfo 发送请求

  • 因为服务端可能出错,返回的批量接口结果可能缺少某个书目信息。我们需要对相关的调用进行抛错,比如 100 毫秒内连续调用:

getBooksInfo(123).then(data => {console.log(data.id)}) // 123
getBooksInfo(456).then(data => {console.log(data.id)}) // 456

我们要归并只调用一次 fetchBooksInfo:

fetchBooksInfo(123456)

如果返回有问题,只返回了:

[{
   id123
   //...
}]

没有返回 id 为 456 的书信息,需要:


getBooksInfo(456).then(data => {console.log(data.id)}).catch(error => {
   console.log(error)
})

捕获错误。

这样一来,我们要对每一个 getBooksInfo 对应的 promise 实例的 reject 和 resolve 方法进行存储,存储在内存 promiseMap 中,以便在合适的时机进行 reject 或 resolve 对应的 promise 实例。

请看代码(对边界 case 的处理省略),我加入了关键注释:


// 储存将要请求的 id 数组
let bookIdListToFetch = []

// 储存每个 id 请求 promise 实例的 resolve 和 reject
// key 为 bookId,value 为 resolve 和 reject 方法,如:
// { 123: [{resolve, reject}]}
// 这里之所以使用数组存储 {resolve, reject},是因为可能存在重复请求同一个 bookId 的情况。其实这里我们进行了滤重,没有必要用数组。在需要支持重复的场景下,记得要用数组存储
let promiseMap = {}

// 用于数组去重
const getUniqueArray = array => Array.from(new Set(array))

// 定时器 id
let timer

const getBooksInfo = bookId => new promise((resolve, reject) => {
   promiseMap[bookId] = promiseMap[bookId] || []
   promiseMap[bookId].push({
       resolve,
       reject
   })

   const clearTask = () => {
       // 清空任务和存储
       bookIdListToFetch = []
       promiseMap = {}
   }

   if (bookIdListToFetch.length === 0) {
       bookIdListToFetch.push(bookId)

       timer = setTimeout(() => {
           handleFetch(bookIdListToFetch, promiseMap)

           clearTask()
       }, 100)
   }
   else {
       bookIdListToFetch.push(bookId)

       bookIdListToFetch = getUniqueArray(bookIdListToFetch)

       if (bookIdListToFetch.length >= 100) {
           clearTimeout(timer)

           handleFetch(bookIdListToFetch, promiseMap)

           clearTask()
       }
   }
})

const handleFetch = (list, map) => {
   fetchBooksInfo(list).then(resultArray => {
       const resultIdArray = resultArray.map(item => item.id)

       // 处理存在的 bookId
       resultArray.forEach(data => promiseMap[data.id].forEach(item => {
           item.resolve(data)
       }))

       // 处理失败没拿到的 bookId
       let rejectIdArray = []
       bookIdListToFetch.forEach(id => {
           // 返回的数组中,不含有某项 bookId,表示请求失败
           if (!resultIdArray.includes(id)) {
               rejectIdArray.push(id)
           }
       })

       // 对请求失败的数组进行 reject
       rejectIdArray.forEach(id => promiseMap[id].forEach(item => {
           item.reject()
       }))
   }, error => {
       console.log(error)
   })
}

做出这道题的关键是:

  • 准确理解题意,因为这个题目完全贴近实际场景需求,准确把控出题者的意图是第一步
  • 对 Promise 熟练掌握
  • 进行 setTimeout 合并 100 毫秒内的请求
  • 存储每个 bookId 的请求 promise 实例,存储该 promise 实例的 resolve 和 reject 方法,以便在批量数据返回时进行对应处理
  • 错误处理