一道经典的Promise面试题

215 阅读4分钟

最近在总结异步的一些实现方式,也是翻出了一道比较经典的 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);
   };
需求

现有如下要求:

  1. fetchBooksInfo 已经给出,但是这个接口单次只支持最多 100 个 id 的查询。
  2. 现需要实现 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
  3. 要考虑服务端出错的情况,比如批量接口请求[123, 446] 书目信息,但是服务端只返回了书目123的信息。此时应该进行合理的错误处理。
  4. 对 ID 重复进行处理
思路

认真读完要求后,来分析下题目,梳理思路:

  1. 首先要对 100 毫秒内的连续请求进行合并,只触发一次请求。考虑可以使用数组存储多条请求的id,并使用定时器处理 100 毫秒的延迟。
  2. 请求的数据长度达到 100 时,清除定时器,直接发送请求。
  3. 还要通过返回的数据判断是否包含全部请求参数中的数据,如果缺失需要抛出错误提示。
  4. 使用 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 出错了
        });

执行结果: 在这里插入图片描述 程序正常抛出错误提示。

完结