原生fetch 实现并发控制

3,561 阅读5分钟

原生fetch 实现并发控制

前端时间面试过程中遇到一道手写题

题目:如何使用原生fetch 实现一个并发控制

要求:

1.通过传入参数进行控制fetch的最大并发量

2. 不影响fetch的正常使用,也就是说在代码中可以将你使用的fetch替换成你写好的函数即可,其他逻辑不动

3. 实现fetch超时设置

举例:
		var url = 'http://192.168.6.41:19010/api/h5/query_repay_order';
var _fetch = handlerFetch(10, 1000);
new Array(50).fill(1).forEach((i, index) => {
  _fetch(url);
});

首先看下这个举例,我们要实现一个handlerFetch函数,该函数接受两个参数,第一个参数控制最大并发量,第二个参数是控制fetch的timeout,时间是毫秒。

handlerFetch函数应该返回的是一promise,这样就可以保证_fetch函数也可以进行链式调用。

再看这个例子,我们可以看到下面调用_fetch是连续调用,控制并发量是10,超时控制是1000ms,根据这些参数,整理一下思路。

思路:
  1. 创建一个handlerFetch函数(参数有两个一个为并发控制,一个为超时设置)

  2. 获取_fetch调用次数count(此处可以考虑使用闭包进行计数)

  3. 比较count和最大并发控制limit 小于limit的请求可以直接执行 大于limit的请求可以推入一个队列

  4. 等待前面fetch请求完成后再执行队列中等待的请求

  5. 超时设置,可以在handlerFetch中统一设置,或是在每一个请求中设置,每一个请求中设置超时优先级高于统一设置(超时可以考虑使用abortController,这个兼容性虽然不好,但是可以真正取消请求)

那我们按照思路一步步进行实现吧。

第一步:实现一个handlerFetch函数
/**
 * 创建一个handlerFetch
 *
 * @param {limit,timeout} limit 为并发控制 timeout为超时设定 
 * @return function 返回一个函数
*/
function handlerFetch (limit, timeout) {
  limit = limit || 1;
  timout = timeout || 0;
  var count = 0, pool = [];
  return function (url) {
    // 其他处理
  }
}
第二步:获取调用_fetch次数
/**
 * 创建一个handlerFetch
 *
 * @param {limit,timeout} limit 为并发控制 timeout为超时设定 
 * @return function 返回一个函数
*/
function handlerFetch (limit, timeout) {
  limit = limit || 1;
  timout = timeout || 0;
  var count = 0, pool = [];
  return function (url) {
    count++;
    console.log(count);
  }
}
// 测试
var _fetch = handlerFetch(10, 1000);
_fetch();		// 1
_fetch();		// 2
_fetch();		// 3
第三步:比较count和最大并发控制limit,小于于limit的直接执行,大于limit的推入pool任务队列
/**
 * 创建一个handlerFetch
 *
 * @param {limit,timeout} limit 为并发控制 timeout为超时设定 
 * @return function 返回一个函数
*/
function handlerFetch (limit, timeout) {
  limit = limit || 1;
  timout = timeout || 0;
  var count = 0, pool = [];
  return function (url) {
    // 每一个fetch请求都是一个task
    var task = () => new Promise((resolve, reject) => {
      fetch(url)
      .then(res => {
        resolve(res);
      })
      .catch(err => {
        reject(err);
      })
    })
  };
  
  // 比较count与limit 大于等于limit的推入等待队列 小于limit的 count + 1,并执行fetch请求
  if (count >= limit) {
    pool.push(task);
  } else {
    ++count;
    task();
  }
}
第四步:等待前面fetch请求完成后再执行队列中等待的请求
/**
 * 创建一个handlerFetch
 *
 * @param {limit,timeout} limit 为并发控制 timeout为超时设定 
 * @return function 返回一个函数
*/
function handlerFetch (limit, timeout) {
  limit = limit || 1;
  timout = timeout || 0;
  var count = 0, pool = [];
  return function (url) {
    // 每一个fetch请求都是一个task
    // 当fetch请求成功或失败的时候 通过next 执行下一个等待队列中的请求
    var task = () => new Promise((resolve, reject) => {
      fetch(url)
      .then(res => {
        resolve(res);
        next();
      })
      .catch(err => {
        reject(err);
        next();
      })
    })
  };
  
  // 定一个next 控制等待队列中的请求继续并发调用
  
  var next = () => {
    // 每执行一次next count - 1,然后比较当前的count 与 limit
    // 如果小于limit 循环执行limit-count 次
    count--;
    if (count < limit && pool.length) {
      var n = limit - count;
      for (var i = 0; i < n; i++) {
        var curTask = pool.shift();
        curTask();
        ++count;
      }
    }
  }
  
  // 比较count与limit 大于等于limit的推入等待队列 小于limit的 count + 1,并执行fetch请求
  if (count >= limit) {
    pool.push(task);
  } else {
    ++count;
    task();
  }
}
第五部:添加超时设置
/**
 * 创建一个handlerFetch
 *
 * @param {limit,timeout} limit 为并发控制 timeout为超时设定 
 * @return function 返回一个函数
*/
function handlerFetch (limit, timeout) {
  limit = limit || 1;
  timeout = timeout || 0;
  var count = 0, pool = [];
  return function (url, options) {
    // 通过AbortController 控制 取消fetch 请求
    var controller = new AbortController();
    var signal = controller.signal;
    // 判断是否需要超时
    var isTimeout = options && options.timeout || timeout
    // 控制请求超时
    var timeoutPromise = () => {
      return new Promise((resolve, reject) => {
        setTimeout(() => {
          resolve('请求超时');
          controller.abort();
        }, options && options.timeout !== undefined ? options.timeout : timeout)
      })
    }
    // 返回fetch 本身
    var taskPromise = () => new Promise((resolve, reject) => {
      fetch(url, { signal, ...options }).then(res => {
        resolve(res);
      }).catch(err => {
        reject(err)
      })
    });
    // 通过Promise.race可以控制超时,并在访问结果中 去继续调用等待池中的请求
    var task = () => (isTimeout ? Promise.race([timeoutPromise(), taskPromise()]) : taskPromise())
        .then((res) => {
          console.log('res', res);
          next();
        })
        .catch(err => {
          next();
          console.log('err', err);
        });
    
     // 定一个next 控制等待队列中的请求继续并发调用
    var next = () => {
      // 每执行一次next count - 1,然后比较当前的count 与 limit
      // 如果小于limit 循环执行limit-count 次
      count--;
      if (count < limit && pool.length) {
        var n = limit - count;
        for (var i = 0; i < n; i++) {
          var curTask = pool.shift();
          curTask();
          ++count;
        }
      }
    };
    // 比较count与limit 大于等于limit的推入等待队列 小于limit的 count + 1,并执行fetch请求
    if (count >= limit) {
      pool.push(task);
    } else {
      ++count;
      task();
    }
  }
}

测试效果1:limit = 1, timeout = 1000

var url = 'http://192.168.6.41:19010/api/h5/query_repay_order'
var _fetch = handlerFetch(1, 1000);

new Array(50).fill(1).forEach((i, index) => {
  _fetch(url)
})

试效果2:limit = 5, timeout = 1000

var url = 'http://192.168.6.41:19010/api/h5/query_repay_order'
var _fetch = handlerFetch(5, 1000);

new Array(50).fill(1).forEach((i, index) => {
  _fetch(url)
})

试效果3:limit = 1, 为每一个fetch设置timeout

var url = 'http://192.168.6.41:19010/api/h5/query_repay_order'
var _fetch = handlerFetch(1);

new Array(50).fill(1).forEach((i, index) => {
  _fetch(url, {timeout: (index + 1) * 200})
})