某条前端面试题--实现一个封装的ajax器(Promise版)

2,361 阅读4分钟

题目描述

实现一个封装ajax器,功能有

  1. 限制一次同时发送的ajax请求数量m个
  2. timeout限制
  3. 重试n次

解决方案

之前的解决方案我放在自己的博客上了,主要是实现队列、可取消的Promise,借此来构建,但ajax器部分的代码耦合性太强了,就不留在这里。

最近看到了如何实现 Promise 的限流: Promise.map 的简单实现,感觉自己写的代码冗长了,像老太婆的裹脚布一般,写一段能够实现功能的松散的代码很简单,但是写简洁易复用易扩展的代码就很困难了。所以在它的基础上重写了一版。我的想法和代码只是尝试回答面试问题,并不是完美的解决方案,但希望也能给你们带去思考,觉得有用可点个赞。

实现思路

  • 去除了队列这个类,没必要,用数组就可以简单实现,但是这样可能会造成性能问题,因为数组会在无限扩充,当增长当一定级别,性能堪忧。
  • 最重要的实现,在外层多套了一个Promise(暂时称为主Promise),外部的异步处理也是在基于主Promise,将主Promise的resolve、reject函数加入队列,但真实的请求是在内部套的Promise(暂时称为子Promise)中,子Promise根据自身的状态来改变主Promise的状态。
  • 限流的实现,也就可以暂时挂起主Promise,不进行真实的请求。等到有一个Promise完成,再去检查队列,执行子Promise。
  • 超时的实现,和之前一样,在子promise放一个定时器,到时候就rejected
  • 重试的实现,维护子Promise,子Promise状态变为rejected,检查下有没有重试机会,有的话,再将给主Promise创建一个子Promise,加入请求队列。这里是实现方式有点不简洁,一直在传参数,待改进。
  • 不过也有点进步,就是将超时和重试这两个功能,写成了可配置的,就是请求时,如果需要错误重试,再传入。如果以后要提供其他功能,再加函数即可,可扩展性提升。

看一下使用方式:

  const mapUrl = function(){
    const a = new Axios(3);
    for (let url of arguments) {
        a.post(url,Axios.timeout(300),Axios.retry(3)).then(value=>{
            console.log(value);
        }).catch((e)=>{
            console.log(url+e.message);
        });
    }
}

mapUrl("baidu1.com","baidu2.com","baidu3.com","baidu4.com","baidu5.com","baidu6.com");

实现代码

这一版的代码少了些

class Axios {
    constructor (n) {
      this.limit = n
      this.count = 0
      this.queue = []
    }
  
    enqueue (fn,promise=null,resolve=null,reject=null) {
      if(promise){
        this.queue.push({fn, resolve, reject});
        this.queue.push(promise);
        return promise;
      }
      //主Promise
      let p = new Promise((resolve, reject) => {
        this.queue.push({fn, resolve, reject })
      })
      this.queue.push(p);
      return p;
    }
  
    dequeue () {
    // 等到 Promise 计数器小于阈值时,则出队执行
      if (this.count < this.limit && this.queue.length) {
        const { fn, resolve, reject} = this.queue.shift();
        const p = this.queue.shift();
        this.run(p, fn,resolve,reject);
      }
    }
  
    // async/await 简化错误处理
    async run (p, fn,resolve,reject) {
      try{
          this.count++
          const value = await fn(p,resolve,reject);
          this.count--;
          this.dequeue();
          //释放主Promise,改变状态为resolved
          resolve(value);
      }catch(e){
          this.count--;
          this.dequeue();
      }
    }
  
    build (fn,promise) {
        let p = this.enqueue(fn,...promise);
        this.dequeue();
        return p;
    }
  
    post(url){
      let fns = [],len = arguments.length-1,parentPromise=[],hasRetry=false;
      if(arguments.length>1){
          url = arguments[0];
          if(len>2&&arguments[len-2] instanceof Promise){
            parentPromise = [].slice.call(arguments,len-2);
            fns = [].slice.call(arguments,0,len-2);
          }else{
            fns = [].slice.call(arguments,0);
          }
      }
      //子Promise,真实请求的地方
      let request = (...parentP)=>{
              let res,rej,promise;
              //模拟post请求,本地测试方便
              promise = new Promise((resolve, reject) => {
                  res = resolve;
                  rej = reject;
                  setTimeout(()=>{
                      resolve(url+"执行成功");
                  }, Math.random()*500);
                  setTimeout(()=>{
                    reject(url+"执行失败");
                }, Math.random()*400);
              })//模拟结束
              //给它加入处理函数(错误重试等)
              for (const fn of fns) {
                  if(typeof(fn) ===  "function"){
                    fn.bind(this)(promise,res,rej,fns,parentP);
                    if(fn.name === Axios.retry.name){
                        hasRetry = true;
                    }
                  }
              }
              //默认加上retry函数,状态改变rejected只在这个函数中处理
              if(!hasRetry){
                Axios.retry(0).bind(this)(promise,res,rej,fns,parentP);
              }
              return promise;
          };
      return this.build(request,parentPromise);
    }
  
  
    //错误重试
    static retry(times){
      return function retry(promise,resolve,reject,args,parentP){
          promise.catch((v)=>{
             if(this instanceof Axios&&times>0){
              for (let a of args) {
                  if(typeof(a) ===  "function" && a.name === Axios.retry.name){
                      times--;
                      a = Axios.retry(times);
                  }
              }
              //失败了,再给它加进请求队列
              console.log(args[0]+"重试");
              this.post(...args,...parentP);
             }else{
                 //释放主Promise,改变状态为rejected
                 parentP[parentP.length-1](new Error("错误重试失败",v));
             }
          });
      }
    }
  
  
    //timeout限制
    static timeout(time){
      return function timeout(promise,resolve,reject,args){
          let t = setTimeout(()=>{
              console.log(args[0]+"超时");
              reject(args[0]+"超时");
          },time);
          promise.then(()=>{
              clearTimeout(t);
              t = null;
          }).catch(()=>{
              clearTimeout(t);
              t = null;
          });
      }
    }
  }

执行结果如下:

作者菜,如有不对,请快点指出,多多见谅!