JavaScript實(shí)現(xiàn)控制并發(fā)請(qǐng)求的方法詳解
題目
現(xiàn)有100個(gè)請(qǐng)求需要發(fā)送,請(qǐng)?jiān)O(shè)計(jì)一個(gè)算法,使用Promise來(lái)控制并發(fā)(并發(fā)數(shù)量最大為10),來(lái)完成100個(gè)請(qǐng)求;
首先先模擬下 100 個(gè)請(qǐng)求:
// 請(qǐng)求列表 const requestList = []; // 為了方便查看,i從1開(kāi)始計(jì)數(shù) for (let i = 1; i <= 100; i++) { requestList.push( () => new Promise(resolve => { setTimeout(() => { console.log('done', i); resolve(i); }, Math.random() * 1000); }), ); }
Promise.all()
初次 看到這個(gè)問(wèn)題,相信大部分同學(xué)第一個(gè)想到的肯定是 Promise.all
,因?yàn)樗亲畛R?jiàn)的并發(fā)請(qǐng)求方式,下面來(lái)實(shí)現(xiàn)一下:
const parallelRun = async max => { const requestSliceList = []; for (let i = 0; i < requestList.length; i += max) { requestSliceList.push(requestList.slice(i, i + max)); } for (let i = 0; i < requestSliceList.length; i++) { const group = requestSliceList[i]; try { const res = await Promise.all(group.map(fn => fn())); console.log('接口返回值為:', res); } catch (error) { console.error(error); } } };
看下效果:
效果不錯(cuò)??!
每次都是并發(fā) 10 個(gè)請(qǐng)求,當(dāng)這 10 個(gè)請(qǐng)求都完成返回時(shí),繼續(xù)下一個(gè) 10 個(gè)請(qǐng)求,完美實(shí)現(xiàn)需求;
可是此時(shí)面試官問(wèn):如果這里邊有一個(gè)請(qǐng)求失敗了會(huì)怎樣?
我:額.......,不確定
面試官:回去等通知吧!
雖然回家等通知了,但這道面試題還是得弄明白,修改下模擬請(qǐng)求,使其隨機(jī)產(chǎn)生一個(gè)錯(cuò)誤,修改如下:
// 請(qǐng)求列表 const requestList = []; for (let i = 1; i <= 100; i++) { requestList.push( () => new Promise((resolve, reject) => { setTimeout(() => { if (i === 92) { reject(new Error('出錯(cuò)了,出錯(cuò)請(qǐng)求:' + i)); } else { console.log('done', i); resolve(i); } }, Math.random() * 1000); }), ); }
控制臺(tái)看下運(yùn)行結(jié)果:
有一個(gè)請(qǐng)求失敗了,這個(gè) Promise.all
就失敗了,沒(méi)有返回值
一組中一個(gè)請(qǐng)求失敗就無(wú)法獲取改組其他成員的返回值,這對(duì)于不需要判斷返回值的情況倒是可以,但是實(shí)際業(yè)務(wù)中,返回值是一個(gè)很重要的數(shù)據(jù)
我們可以接受某個(gè)接口失敗了沒(méi)有返回值,但是無(wú)法接受一個(gè)請(qǐng)求失敗了,跟它同組的其他 9 個(gè)請(qǐng)求也沒(méi)有返回值
既然,失敗的請(qǐng)求會(huì)打斷 Promise.all
,那有沒(méi)有一種方法可以不被失敗打斷呢?
還真有,它就是 Promise.allSettled
!
Promise.allSettled()
先來(lái)看下權(quán)威的 MDN 的介紹
Promise.allSettled() 方法是 promise 并發(fā)方法之一。在你有多個(gè)不依賴于彼此成功完成的異步任務(wù)時(shí),或者你總是想知道每個(gè) promise 的結(jié)果時(shí),使用 Promise.allSettled()
簡(jiǎn)單說(shuō)就是:每個(gè)請(qǐng)求都會(huì)返回結(jié)果,不管失敗還是成功
使用 Promise.allSettled()
替換下 Promise.all()
:
const parallelRun = async max => { const requestSliceList = []; for (let i = 0; i < requestList.length; i += max) { requestSliceList.push(requestList.slice(i, i + max)); } for (let i = 0; i < requestSliceList.length; i++) { const group = requestSliceList[i]; try { // 使用 allSettled 替換 all const res = await Promise.allSettled(group.map(fn => fn())); console.log('接口返回值為:', res); } catch (error) { console.error(error); } } };
看下返回結(jié)果:
可以看到,接口全部正常有返回值,返回值中會(huì)正常記錄當(dāng)前請(qǐng)求時(shí)成功還是失敗
不錯(cuò)哦,感覺(jué) Promise.allSettled()
就是最優(yōu)解了!
此時(shí)面試官又問(wèn):那如果有一個(gè)請(qǐng)求非常耗時(shí),會(huì)出現(xiàn)什么情況?
答:有一個(gè)請(qǐng)求非常耗時(shí),那組的請(qǐng)求返回就會(huì)很慢,會(huì)阻塞了后續(xù)的接口并發(fā)。
面試官:有沒(méi)有什么方法可以解決這個(gè)問(wèn)題?
我 :額...... 不知道......
面試官:回去等通知吧~~~
最優(yōu)解
分析問(wèn)題
使用 Promise.all()
或是 Promise.allSettled()
,每次并發(fā) 10 個(gè)請(qǐng)求,確實(shí)可以滿足并發(fā)要求,但是效率較低:如果存在一個(gè)或多個(gè)慢接口,那么會(huì)出現(xiàn)以下兩個(gè)問(wèn)題:
- 有慢接口的并發(fā)組返回會(huì)很慢,一個(gè)慢接口拖慢了其他 9 個(gè)接口,得不償失
- 本來(lái)我們是可以并發(fā) 10 個(gè)請(qǐng)求的,但是一個(gè)慢接口導(dǎo)致該組的其他 9 個(gè)并發(fā)位置都被浪費(fèi)了,這會(huì)導(dǎo)致這 100 個(gè)接口的并發(fā)時(shí)間被無(wú)情拉長(zhǎng)
- 慢接口組后續(xù)的并發(fā)組都被阻塞了,更慢了
解決方法
有沒(méi)有辦法解決上述問(wèn)題呢,答案是肯定的:
可以維護(hù)一個(gè)運(yùn)行池和一個(gè)等待隊(duì)列,運(yùn)行池始終保持 10 個(gè)請(qǐng)求并發(fā),當(dāng)運(yùn)行池中有一個(gè)請(qǐng)求完成時(shí),就從等待隊(duì)列中拿出一個(gè)新請(qǐng)求放到運(yùn)行池中運(yùn)行,這樣就可以保持運(yùn)行池始終是滿負(fù)荷運(yùn)行,即使有一個(gè)慢接口,也不會(huì)阻塞后續(xù)的接口入池
代碼實(shí)現(xiàn)
// 運(yùn)行池 const pool = new Set(); // 等待隊(duì)列 const waitQueue = []; /** * @description: 限制并發(fā)數(shù)量的請(qǐng)求 * @param {*} reqFn:請(qǐng)求方法 * @param {*} max:最大并發(fā)數(shù) */ const request = (reqFn, max) => { return new Promise((resolve, reject) => { // 判斷運(yùn)行吃是否已滿 const isFull = pool.size >= max; // 包裝的新請(qǐng)求 const newReqFn = () => { reqFn() .then(res => { resolve(res); }) .catch(err => { reject(err); }) .finally(() => { // 請(qǐng)求完成后,將該請(qǐng)求從運(yùn)行池中刪除 pool.delete(newReqFn); // 從等待隊(duì)列中取出一個(gè)新請(qǐng)求放入等待運(yùn)行池執(zhí)行 const next = waitQueue.shift(); if (next) { pool.add(next); next(); } }); }; if (isFull) { // 如果運(yùn)行池已滿,則將新的請(qǐng)求放到等待隊(duì)列中 waitQueue.push(newReqFn); } else { // 如果運(yùn)行池未滿,則向運(yùn)行池中添加一個(gè)新請(qǐng)求并執(zhí)行該請(qǐng)求 pool.add(newReqFn); newReqFn(); } }); }; requestList.forEach(async item => { const res = await request(item, 10); console.log(res); });
效果
可以看到,100 個(gè)接口不斷執(zhí)行,并沒(méi)有任何等待或是被阻塞的現(xiàn)象,完美!
其他優(yōu)秀庫(kù)
社區(qū)已有很多優(yōu)秀的并發(fā)限制庫(kù),這里重點(diǎn)介紹下 p-limit
安裝:
npm install p-limit -S
使用方法:
import plimit from 'p-limit'; const limit = plimit(10); requestList.forEach(async item => { const res = await limit(item); console.log(res); });
運(yùn)行效果與上面的隊(duì)列的運(yùn)行效果是一致的。下面看下庫(kù)源碼(精簡(jiǎn)后):
import Queue from 'yocto-queue'; export default function pLimit(concurrency) { const queue = new Queue(); let activeCount = 0; const next = () => { activeCount--; if (queue.size > 0) { queue.dequeue()(); } }; const run = async (function_, resolve, arguments_) => { activeCount++; const result = (async () => function_(...arguments_))(); resolve(result); try { await result; } catch {} next(); }; const enqueue = (function_, resolve, arguments_) => { queue.enqueue(run.bind(undefined, function_, resolve, arguments_)); (async () => { // This function needs to wait until the next microtask before comparing // `activeCount` to `concurrency`, because `activeCount` is updated asynchronously // when the run function is dequeued and called. The comparison in the if-statement // needs to happen asynchronously as well to get an up-to-date value for `activeCount`. await Promise.resolve(); if (activeCount < concurrency && queue.size > 0) { queue.dequeue()(); } })(); }; const generator = (function_, ...arguments_) => new Promise(resolve => { enqueue(function_, resolve, arguments_); }); return generator; }
短短 60 行代碼就實(shí)現(xiàn)了一個(gè)功能強(qiáng)大的并發(fā)處理庫(kù),真是厲害,下面分析下具體實(shí)現(xiàn):
- 首先 p-limit 庫(kù)默認(rèn)導(dǎo)出一個(gè)函數(shù)
pLimit
,該函數(shù)接收一個(gè)數(shù)字,表示最大并發(fā)數(shù) pLimit
函數(shù)函數(shù)返回一個(gè)generator
函數(shù),該函數(shù)返回一個(gè)Promise
,并且其中調(diào)用了enqueue
函數(shù)enqueue
函數(shù)主要是將run
函數(shù)加入隊(duì)列queue
中,之后判斷下activeCount < concurrency && queue.size > 0
,表示當(dāng)前隊(duì)列大小小于最大并發(fā)數(shù)且隊(duì)列不為空,則需要從隊(duì)列中取出一個(gè)請(qǐng)求執(zhí)行,即執(zhí)行run
函數(shù)run
函數(shù)執(zhí)行時(shí)需要先將activeCount
加一,之后執(zhí)行真正的請(qǐng)求函數(shù)(async () => function_(...arguments_))()
- 之后等待請(qǐng)求完成
await result;
之后執(zhí)行next
函數(shù) next
函數(shù)主要從隊(duì)列中取出一個(gè)新請(qǐng)求執(zhí)行并將activeCount
減一
總結(jié)
本文主要總結(jié)了 100 個(gè)請(qǐng)求限制并發(fā)的方法:
Promise.all()
最簡(jiǎn)單的控制并發(fā),但是請(qǐng)求出錯(cuò)會(huì)導(dǎo)致該組無(wú)返回值Promise.allSettled()
解決了Promise.all()
的問(wèn)題,但是卻存在慢接口阻塞后續(xù)請(qǐng)求,且浪費(fèi)其余并發(fā)位置的問(wèn)題- 通過(guò)維護(hù)一個(gè)運(yùn)行池,當(dāng)運(yùn)行池中有請(qǐng)求完成時(shí)便從等待隊(duì)列中取一個(gè)心情求入池執(zhí)行,直到所有的請(qǐng)求都入池
- 介紹了社區(qū)的
p-limit
庫(kù)的使用方法和實(shí)現(xiàn)原理
到此這篇關(guān)于JavaScript實(shí)現(xiàn)控制并發(fā)請(qǐng)求的方法詳解的文章就介紹到這了,更多相關(guān)JavaScript控制并發(fā)請(qǐng)求內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- JS前端并發(fā)多個(gè)相同的請(qǐng)求控制為只發(fā)一個(gè)請(qǐng)求方式
- JavaScript使用Promise實(shí)現(xiàn)并發(fā)請(qǐng)求數(shù)限制
- JS使用Promise控制請(qǐng)求并發(fā)數(shù)
- JavaScript使用Promise控制并發(fā)請(qǐng)求
- JS循環(huán)發(fā)送請(qǐng)求時(shí)控制請(qǐng)求并發(fā)數(shù)實(shí)例
- 詳解JavaScript如何控制并發(fā)請(qǐng)求數(shù)量
- JavaScript實(shí)現(xiàn)控制并發(fā)請(qǐng)求數(shù)量的方法詳解
相關(guān)文章
js正則表達(dá)式最長(zhǎng)匹配(貪婪匹配)和最短匹配(懶惰匹配)用法分析
這篇文章主要介紹了js正則表達(dá)式最長(zhǎng)匹配(貪婪匹配)和最短匹配(懶惰匹配)用法,結(jié)合實(shí)例形式分析了貪婪匹配與懶惰匹配的具體用法與相關(guān)注意事項(xiàng),需要的朋友可以參考下2016-12-12javascript實(shí)現(xiàn)5秒倒計(jì)時(shí)并跳轉(zhuǎn)功能
這篇文章主要為大家詳細(xì)介紹了javascript實(shí)現(xiàn)5秒倒計(jì)時(shí)并跳轉(zhuǎn)功能,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-06-06JS實(shí)現(xiàn)點(diǎn)擊拉拽輪播圖pc端移動(dòng)端適配
本文通過(guò)實(shí)例代碼給大家介紹了JS點(diǎn)擊拉拽輪播圖pc端移動(dòng)端適配 ,非常不錯(cuò),具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2018-09-09JS實(shí)現(xiàn)OCX控件的事件響應(yīng)示例
JS支持OCX控件的事件(event),當(dāng)OCX控件定義的事件發(fā)生時(shí),JS可以捕獲該事件并對(duì)事件進(jìn)行相應(yīng)的處理2014-09-09跟我學(xué)習(xí)javascript的循環(huán)
跟我學(xué)習(xí)javascript的循環(huán),本文不僅針對(duì)javascript循環(huán)進(jìn)行講解,還對(duì)prototype補(bǔ)充了幾點(diǎn)小tips,歡迎大家閱讀。2015-11-11JavaScript關(guān)于某元素點(diǎn)擊事件的監(jiān)聽(tīng)和觸發(fā)
本文主要介紹了JavaScript關(guān)于某元素點(diǎn)擊事件的監(jiān)聽(tīng)和觸發(fā),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2022-07-07JavaScript中find()、findIndex()、filter()、indexOf()處理數(shù)組方法的具體區(qū)別詳
在JavaScript中數(shù)組是一種非常常見(jiàn)且功能強(qiáng)大的數(shù)據(jù)結(jié)構(gòu),這篇文章主要介紹了JavaScript中find()、findIndex()、filter()、indexOf()處理數(shù)組方法具體區(qū)別的相關(guān)資料,文中通過(guò)代碼介紹的非常詳細(xì),需要的朋友可以參考下2025-04-04