在项目中,我们经常会遇到同时发送多个HTTP请求的情况,对于同域名的请求,浏览器会限制同时发起的请求数量,当超过浏览器并发请求限制时,超过的请求将会等待,直到有请求返回时才会进行下一次请求。这是浏览器的一种保护机制,为了防止通过浏览器发起过量的请求,耗尽资源。在小程序中也会有同样的机制,例如,微信小程序中说明 wx.request
的最大并发限制是 10 个,wx.connectSocket
的最大并发限制是 5 个(参见 微信小程序官方文档-基础能力/网络/使用说明-网络-3.网络请求-使用限制 ),但是这里有个BUG,在小程序中如果并发请求超过10个,并不会等待而是直接失败,所以实际上需要用户自己处理并发请求数,在超过10个时,请求需要在队列中等待而不是直接失败。
假如我们的请求方法 request
形如:
1 2 3 4 5 6 7 8 9 function request (options ) { return new Promise ((resolve, reject ) => { console .log (`${options} is start` ) setTimeout (() => { console .log (`${options} is finished` ) resolve ({ data : options }); }, 3000 ); }); }
request
是一个通用的请求方法,它接受请求的参数,返回请求的 Promise,其中 options 是形如 {url: '', method: 'GET', params: {}}
的请求相关参数对象。
在业务中,我们会通过 fetch
方法获取请求内容并处理请求结果:
1 2 3 4 5 6 7 function fetch (options ) => { request (options).then ((res ) => { }).catch ((err ) => { }); }
fetch
是一个和业务相关的请求方法,它接受请求的参数,对请求的结果进行处理。
当发起多个并行请求时,最简单直接的方法就是使用 Promise.all
:
1 2 3 4 5 function parallelRequest (optionsList ) { return Promise .all (optionsList.map (options => request (options))); } parallelRequest ([1 ,2 ,3 ]).then (console .log );
如果需要请求串行发送,可以通过 async/await
的方式等待前一个请求返回后再发起下一个请求:
1 2 3 4 5 6 7 8 9 10 async function serialRequest (optionsList ) { let res = []; for (let i = 0 ; i < optionsList.length ; i++) { let result = await request (optionsList[i]); res.push (result); } return Promise .resolve (res); } serialRequest ([1 ,2 ,3 ]).then (console .log );
串行是可以避免超过10个请求后,再次发送请求失败的问题,但是没有充分利用并行发送的数量,更好的方式是通过参数限制并发请求数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 function multiRequest (optionsList = [], maxNum ) { const len = optionsList.length ; const result = new Array (len).fill (false ); let count = 0 ; return new Promise ((resolve, reject ) => { while (count < maxNum) { next (); } function next ( ) { let current = count++; if (current >= len) { !result.includes (false ) && resolve (result); return ; } const options = optionsList[current]; request (options) .then ((res ) => { result[current] = res; if (current < len) { next (); } }).catch ((err ) => { result[current] = err; if (current < len) { next (); } }); } }); } multiRequest ([1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 , 10 ], 3 ).then (console .log );
实际业务场景中,我们并不会在一个地方同时发起多个请求,而是在不同的地方发起请求,所以限制并发数量的的 request 方法应该是一个工具函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 class LimitRequest { constructor (limit ) { this .limit = limit; this .tasks = []; this .current = 0 ; } addRequest (reqFn ) { if (!reqFn || !(reqFn instanceof Function )) { console .error ('当前请求不是一个Function' ); return ; } this .tasks .push (reqFn); if (this .current < this .limit ) { this .run (); } } async run ( ) { try { this .current ++; const fn = this .tasks .shift (); await fn (); } catch (err) { throw new Error (err); } finally { this .current --; if (this .tasks .length > 0 ) { this .run (); } } } } let limitRequest = new LimitRequest (3 );for (let i = 1 ; i <= 10 ; i++) { limitRequest.addRequest (fetch.bind (null , i)); }
注意上面用的是 fetch 方法,当需要发送请求时,需要确定请求返回后的处理逻辑,将整体的方法写成 fetch 方法,并通过 addRequest
添加到并行的队列中去。
我们也可以优化一下,使用 request 方法,这样可以在请求时不必确定返回后的逻辑,将 请求 这个动作与业务分离:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 class LimitRequest { constructor (limit ) { this .limit = limit; this .tasks = []; this .current = 0 ; } request (options ) { return new Promise ((resolve, reject ) => { let reqFn = request.bind (null , options); this .tasks .push ([reqFn, resolve, reject]); if (this .current < this .limit ) { this .run (); } }); } async run ( ) { try { this .current ++; const [fn, resolve, reject] = this .tasks .shift (); await fn ().then (res => { resolve (res); }).catch (err => { reject (err); }); } catch (err) { throw new Error (err); } finally { this .current --; if (this .tasks .length > 0 ) { this .run (); } } } } let limitRequest = new LimitRequest (3 );let newRequest = limitRequest.request .bind (limitRequest);for (let i = 1 ; i <= 10 ; i++) { newRequest (i).then (console .log ); }
作为 utils 方法,可以这样写:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 let limit = 3 ; let tasks = []; let current = 0 ; export function limitRequest (options ) { return new Promise ((resolve, reject ) => { let reqFn = request.bind (null , options); tasks.push ([reqFn, resolve, reject]); if (current < limit) { run (); } }); } export function setLimit (num ) { limit = num; } async function run ( ) { try { current++; const [fn, resolve, reject] = this .tasks .shift (); await fn ().then (res => { resolve (res); }).catch (err => { reject (err); }); } catch (err) { throw new Error (err); } finally { current--; if (tasks.length > 0 ) { run (); } } } for (let i = 1 ; i <= 10 ; i++) { limitRequest (i).then (console .log ); }