浏览器并发请求

610 阅读5分钟

并发请求

概念:在同一时间内发出多个请求

并发请求可以针对不同的资源或者服务,其目的是为了提高文件传输效率或者请求效率。并发是计算机领域中的一个重要概念,它指的是在同一时间内处理多个任务或者请求的能力。这种能力是现代计算机系统设计的核心,它影响着系统的性能、可伸缩性和稳定性。

1. 控制并发请求上限

1.1 请求计数器

/**
* requestList 任务队列
* limits 并发数量上限
* callback 回调函数
*/

function sendRequest(requestList, limits, callback) {
    // 当前执行队列
    const requestListWrapperedQueue = [];
    // 初次执行时的并发数
    const concurrentNum = Math.min(limits, requestList.length);
    // 当前任务并发数
    let concurrentCount = 0;
    // 初次启动任务
    const runTaskNeeded = () => {
        let i = 0;
        while (i < concurrentNum) {
            i++;
            runTask();
        }
    };
    // 取出单个任务并执行
    const runTask = async () => {
        const task = requestListWrapperedQueue.shift();
        if (task) {
            try {
                concurrentCount++;
                const res = await task();
            } catch (err) {
                return err;
            } finally {
                concurrentCount--;
                if (concurrentCount < limits &&requestListWrapperedQueue.length > 0) {
                    runTask();
                }
            }
        }
    };
    runTaskNeeded();
}

1.2 Promise方式

function sendRequest(requestList, limits, callback) {
    // 任务队列
    const promises = [];
    // 当前的并发池
    const pool = new Set([]);
    
    const runTask = async () => {
        // for await of 循环执行并发池
        for (let requestItem of requestList) {
            if (pool.size >= limits) {
                await Promise.race(pool).catch((err) => err);
            }
            const promise = requestItem();
            const cb = () => {
                pool.delete(promise);
            };
            promise.then(cb, cb);
            pool.add(promise);
            promises.push(promise);
        }
        // 通过allSettled获取异步任务的执行结果
        Promise.allSettled(promises).then(callback,callback);
    };
    runTask();
}
  • 减少变量记录当前并发执行的请求数量
  • 利用Set数据结构避免重复触发同一个请求
  • 通过Promise.race将请求池数量降到限制以下
  • 通过.then中的回调函数完成任务的清除
  • 通过Promise.allSettled获取所有的异步结果

promise.allSettled

const p1 = new Promise((resolve,reject)=>{
    setTimeout(()=>{
        resolve('success p1');
    },10)
});
const p2 = new Promise((resolve,reject)=>{
    setTimeout(()=>{
        resolve([1,2,3,4]);
    },10)
});
const p3 = new Promise((resolve,reject)=>{
    setTimeout(()=>{
        resolve({name:'lili',age:5});
    },10)
});
const p4 = new Promise((resolve,reject)=>{
    setTimeout(()=>{
        reject('error p4');
    },10)
})
Promise.allSettled([p1,p2,p3,p4]).then((data)=>{
    console.log(data,'data');
},(err)=>{
    console.log(err,'err');
});

image.png

2. 控制单个请求重复次数

/**
* requestList 任务队列
* limits 并发数量上限
* callback 回调函数
* retryTimes 任务重试次数
*/

function sendRequest(requestList, limits, callback, retryTimes) {
    const requestListWrapperedQueue = [];
    const concurrentNum = Math.min(limits, requestList.length);
    const returnPromises = [];
    let concurrentCount = 0;
    // 1. 包裹一层promise,并且将相关信息重新包装放入请求队列
    const wrapePromise = (requestItem) => {
        return new Promise((resolve, reject) => {
            requestListWrapperedQueue.push({
                requestItem,
                resolve,
                reject,
                remainRetryTime: retryTimes,
            });
        });
    };
    const runTaskNeeded = () => {
        let i = 0;
        while (i < concurrentNum) {
            i++;
            runTask();
        }
    };
    const runTask = async () => {
        const task = requestListWrapperedQueue.shift();
        if (task) {
            const { requestItem, resolve, reject, remainRetryTime } = task;
            try {
                concurrentCount++;
                const res = await requestItem();
                resolve(res);
            } catch (err) {
                // 2. 判断是否可以再进行下次请求
                if (remainRetryTime > 0) {
                    requestListWrapperedQueue.push(requestItem);
                    // 3. 更新剩余次数
                    task.remainRetryTime--;
                } else {
                    reject(err);
                }
            } finally {
                concurrentCount--;
                if (concurrentCount < limits &&requestListWrapperedQueue.length > 0) {
                    runTask();
                }
            }
        }
    };
    // 4. 构建执行队列,将每个任务都用promise包装
    const init = () => {
        for (let requestItem of requestList) {
            const wrapperedPromise = wrapePromise(requestItem);
            returnPromises.push(wrapperedPromise);
        }
    };
    const start = () => {
        init();
        runTaskNeeded();
    };
    start();
    // 5. 通过allSettled获取异步任务的执行结果
    Promise.allSettled(returnPromises).then(callback, callback);
}

3. 浏览器的并发请求

浏览器的并发请求是指浏览器能够同时发送的 HTTP 请求的数量。

为了避免对服务器造成过大的负载压力,浏览器对并发请求数量做了限制。

浏览器HTTP1.0HTTP1.1
IE6、724
IE866
火狐66
Safari44
谷歌66
Opera44

HTTP0.9、HTTP1.0每个请求都单独建立一个TCP连接,请求完成后连接断开; HTTP1.1可以持久连接,TCP建立连接后不会立即关闭,多个请求可以复用同一个TCP连接,而且多个请求可以并行发送。

浏览器对于并发请求的规则如下所示(同一域名下生效):

  • 相同的 GET 请求的并发数是1,上一个请求结束,才会执行下一个请求,否则置入队列等待发送
  • 不同的 GET/POST 请求的并发数量是6,当发送的请求数量达到6个,并且都没有得到响应时,后面的请求会置入队列等待发送

HTTP1.1持久链接和HTTP2.0多路复用的区别:

  • HTTP1.1持久链接:多个请求共用一个TCP链接,但是同时只能发送一个请求
  • HTTP2.0多路复用:在一个TCP链接中,可以同时发送多个请求

4. 取消已发送的请求

4.1 取消axios请求

axios提供了CancelTokencanceTokenSource来实现请求的取消。

使用CancelToken可以创建一个用于取消请求的令牌,而CancelTokenSource 可以用于一次性取消多个请求。

import axios from 'axios';

// 创建一个 CancelToken.source 实例
const { token, cancel } = axios.CancelToken.source();

取消单个请求

// 发送请求时,将 cancelToken 传递给 Axios 的 request 方法
axios.request({
    url:'https://api.example.com/data',
    method: 'get',
    cancelToken: token
}).then(response => {}).catch(error => {
    if(axios.isCancel(error)) {
        console.log("请求已取消",error.message);
    }else {
        console.error('请求出错',error);
    }
})
cancel('请求取消原因');

当调用 cancel 函数时,会导致与 token 相关联的请求被取消。在请求被取消时,Axios 会抛出一个 Cancel 错误,我们可以通过axios.isCancel(error) 方法来判断是否是取消请求导致的错误,并进行相应的处理。

取消多个请求

// 创建所有请求
const request1 = axios.get('/api/data1', { cancelToken: token });
const request2 = axios.get('/api/data2', { cancelToken: token });
const request3 = axios.get('/api/data3', { cancelToken: token });
// 同时发送多个请求
axios.all([request1,request2,request3])
    .then(axios.spread((response1,response2,response3) => {  
        console.log(response1.data);
        console.log(response2.data);
        console.log(response3.data);
    ))}
    .catch(error => {
        if(axios.isCancel(error)){
            console.log('请求被取消',error.message);
        }else{
            console.log('请求失败',error.message);
        }
    });

// 取消所有请求
cancel('用户取消操作');

选择性取消某个请求

const CancelToken = axios.CancelToken;
const source1 = CancelToken.source();
const source2 = CancelToken.source();
 
function clearApi1Fn() {
    // 用于取消第一个接口的请求
    source1.cancel('Operation canceled by the user.');
}
function clearApi2Fn() {
    // 用于取消第二个接口的请求
    source2.cancel('Operation canceled by the user.');
}

4.2 取消fetch请求

AbortController可以终止一个或者多个fetch请求。

// 创建一个 AbortController 实例,中文名为中断控制器
const controller = new AbortController();
const signal = controller.signal;

取消单个请求

// 发送请求时,将signal传递给fetch或XMLHttpRequest
fetch('https://api.example.com/data', { signal })
.then(response => {})
.catch(error => {
    if (error.name === 'AbortError'){
        console.log("请求已取消");
    } else {
        console.error( '请求出错',error);
    }
})
// 取消请求
controller.abort();

当调用 controller.abort() 方法时,会导致与 signal 相关联的请求被取消。在请求被取消时,fetch 或 XMLHttpRequest会抛出一个 AbortError 错误,我们可以在 catch 中捕获这个错误并进行相应的处理。

通过使用 AbortController 和 Abortsignal,我们可以在浏览器中比较方便地取消已经发送的请求,避免不必要的网络流量和资源浪费。

取消多个请求

// 发送多个请求
Promise.all([
    fetch(url1, { signal }), 
    fetch(url2, { signal }),
    fetch(url1, { signal })
]).then((data) => { 
    const [result1,result2,result3] = data;
}).catch(error => {
    ... ...
})

// 取消请求
controller.abort();