先直接看效果↓↓↓
![](https://img.laitimes.com/img/__Qf2AjLwojIjJCLyojI0JCLiAnYldHL0FWby9mZvwFN4ETMfdHLkVGepZ2XtxSZ6l2clJ3LcV2Zh1Wa9M3clN2byBXLzN3btgHL9s2RkBnVHFmb1clWvB3MaVnRtp1XlBXe0xCMy81dvRWYoNHLwEzX5xCMx8FesU2cfdGLwMzX0xiRGZkRGZ0Xy9GbvNGLpZTY1EmMZVDUSFTU4VFRR9Fd4VGdsQTMfVmepNHLrJXYtJXZ0F2dvwVZnFWbp1zczV2YvJHctM3cv1Ce-cmbw5CM0UDM0cDNjFWO4ImZjljZyYzX4QjN0ITM1EzLchDMyIDMy8CXn9Gbi9CXzV2Zh1WavwVbvNmLvR3YxUjLyM3Lc9CX6MHc0RHaiojIsJye.png)
廢話連篇
我們都知道,浏覽器在發送請求的時候,有最大并發數量的限制,如果在我們的網頁中同時有大量的資源需要加載,那麼浏覽器不會同時加載這些資料,而是先發出一部分請求,等到有一個請求資料響應完畢的時候,才會發出另一個請求。
這裡的最大并發請求數量不是越大或者越小就好,需要找到一個相對的平衡點,因為并發數越少,那麼各個請求之間發生阻塞的次數就會增多,導緻頁面加載過慢,影響使用者體驗,這就好比我們從北京到上海,如果可乘坐的車次很少,那麼大量的人員就有可能搶不到票,導緻我們隻能延期出行,隻能等待上次列車傳回之後才能乘坐下一次列車。
那麼是不是可乘坐的列車次數
(放到浏覽器中就是請求并發數)
越大就越好呢?顯然不是,列車需要鐵軌與從業人員,并且要處理好排程事宜,同樣并發越大伺服器承受的壓力越大,解析請求的時間越耗時。是以适當的增加每趟列車的車廂數,就可以在列車數與運載量之間找到平衡,放到實際的項目中就是合并我們的js、css等檔案,将多個請求合并為一個請求,達到優化網站的目的。
(Tips:該限制是針對同一個域名作用的,可以使用不同子域名的方式來突破這個限制。)
目前浏覽器中對這個的限制數量一般為6個。
那麼我們能不能通過代碼來實作一個類似的功能,自己定義一個方法來控制最大的并發請求數量呢?接下來我們就嘗試做到這一點。
目的
建立一個待請求隊列queue,根據我們設定的最大并發請求數量max,來同時發起max個請求,當其中有任意一個請求響應并傳回值的時候,我們将隊列中的下一個請求立即發送,以此類推,直到所有請求全部發送完畢。可以将所有請求的傳回結果緩存起來,然後當所有請求全部響應完成之後,傳回一個結果數組,按照相應的隊列順序把結果抛給調用者,并且執行預先設定好的回調函數,如果存在的話。
建立請求
首先,我們需要模拟出來一大堆異步請求,當然是選擇promise來處理這項工作啦。為了簡單起見,可以通過一個函數建立待執行的promise,并在promise裡面執行相應異步操作。
function createPromise (num) {
console.log(`生成第${num}個promise`)
return function () {
return new Promise((resolve, reject) => {
let time = Math.floor(Math.random() * 90) + 10
console.log(`執行第${num}個promise,需耗時${time}毫秒`)
setTimeout(() => {
console.log(`已完成請求${num}`)
resolve({
order: num,
data: `結果${num}`
})
}, time)
})
}
}
為了友善示範,我們定義一個createPromise函數,它傳回一個待執行函數,這個函數會傳回一個promise,通過傳入一個num參數來作為區分不同異步操作的辨別符,實際項目中可以直接對多個異步操作進行控制,不需要多包裹的一層function。
其中promise裡面,我們定義了一個随機的接口請求時間time,設定為10ms~100ms之間,來模拟我們的真實請求環境,因為各個接口的實際通路時間都是不盡相同。然後利用setTimeout來模拟異步操作,當在對應time時間内接口相應完成之後,将會把得到的傳回值resolve出去,以便做進一步的處理。
這裡resolve的結果是一個對象,order屬性用來辨別該異步操作的身份,data屬性用來存放異步操作的結果。
[ok]這裡沒什麼技術含量,現在我們通過這個函數來建立我們需要的東西。
加入隊列
接下來,我們通過一個循環将多次生成的函數傳回值存放到一個隊列裡面,以供我們将來使用。
var queue = []
for (let i = 1; i < 11; i++) {
queue.push(createPromise(i))
}
好了,現在queue數組裡面存放了10個待執行函數,到此為止我們的準備工作已經做好了。
發送請求
異步請求隊列準備完畢,那麼我們需要建立一個limitRequest函數,來處理我們最開始的訴求,我們希望,這個函數應該是這樣調用的。
limitRequest(queue, 4, function (data) {
console.log(data)
console.log("所有請求全部執行完畢")
})
limitRequest第一個參數為我們剛建立的請求隊列,第二個參數為我們要限制的最大并發請求數量,第三個參數為全部請求執行完畢的回調函數,該函數傳入的參數為所有請求傳回值組成的一個數組,結果的位置與相應的請求在隊列中的位置一一對應。
現在我們就來構造這個函數,該如何入手呢?咳咳~,忍不住想說句題外話。[淚奔]
任何的項目,代碼層面都是最不重要的,到了寫代碼這一步的時候,勞動力都是最廉價的,從項目角度來說,業務!業務!業務!才是最重要的,隻有理清了業務我們才能從更高的角度來設計出一個優秀的系統。從單個的問題或者需求來說,不斷的分析或者推演之後得到的結論才能更好的指導我們進行開發。
舉一個不太恰當的例子,就比如管中窺豹,如果我們隻通過一根管子來看對面的物體,不移動管子,隻看了一眼之後,我們就描繪它的形狀,根據我們的經驗,我們會覺得這是一頭豹子,但是其實它可能是一隻斑點狗或者一隻大花貓,甚至可能是其他的什麼動物或物體,當我們移動一下發現自己錯了的時候,那麼就需要修正自己的見解,如此反複下去就像我們寫代碼一樣,bug反反複複的,這裡看着對了,但是那裡又錯了。如果我們一開始不着急下結論,而是同樣拿着這個管子圍繞這個物體上下左右前後多個角度仔細的觀察之後再下結論,那麼正确的機率将極大的增加,并且後期隻需要極小的修補就可以打到滿意的效果。
(⊙o⊙)…
言歸正傳,回憶一下剛才我們所做的事情,分析一下我們要做的處理,發起多個請求→等待一個傳回→追加一個請求→繼續等待傳回。其實這是一個不斷的控制權轉讓的問題。那麼我們考慮使用generator來幫助我們完成這個事情,并且我們要做好計數,即當請求數達到最大的時候就停止繼續發送請求,當有請求傳回的時候,我們繼續恢複發送請求,直到所有請求全部執行完畢,然後将獲得到的結果全部放在數組裡面傳回出去,在這期間我們要把獲得的結果緩存起來,放在數組裡面,并在最後通知generator函數結束執行。根據這個思路,請看下面的代碼,我們會發現非常的清晰。[靈光一閃]
function limitRequest (q, max, cb) {
console.log(`請求最大并發數控制為${max}`)
let request = generatorRequest(q)
let sent = 0
let received = 0
let result = []
let size = Math.min(q.length, max)
function runIt () {
request.next().value.then(value => {
result[value.order - 1] = value.data
received++
if (sent < q.length) {
sent++
runIt()
} else if (received == q.length) {
request.return()
cb && cb(result)
}
})
}
while (sent < size) {
sent++
runIt()
}
}
天才不是我,是你,給自己一個掌聲。[鼓掌]
其中generatorRequest函數就是我們剛才說的generator,我們把對它的控制權用request來表示。我們先不必關心它,下面會講到。sent表示已經發送的請求數量,received表示已經響應的請求數量,result表示傳回結果的數組,其中size用來取所有待請求的數量與所設定的最大數量中的最小值,接下來就是核心點runIt函數。
request.next()将會執行generator函數并在yield表達式處釋放控制權,并将yield後面的結果存放在value裡面,由于得到的結果是一個promise,是以我們可以通過then來繼續處理接下來的工作,主要做了三件事情:
- 緩存結果,為了使結果對應,是以不能用push處理,因為我們無法控制結果的傳回順序。
- 如果未全部執行完畢,那麼繼續執行runIt。
- 執行完畢則結束generator,然後執行傳入的回調函數,并将得到的結果回傳過去。
其中while用來處理第一發出的最大數量的所有請求,後續的調用都通過runIt回調就可以了。
神奇的generator
最終我們就來揭開generator神秘的面紗,其實很簡單。[吐舌]
function* generatorRequest (q) {
let t = 0
while (true) {
yield q[t++]()
}
}
喏~,就這麼幾行,哈哈哈哈哈哈,其實這裡偷了一個懶,直接初始化t之後就讓它單純的自增了,根據generator函數的性質,還是可以做到很多更精細化和好玩的控制的。
現在我們執行一下就會得到上面最開始的那張截圖的例子的效果了,也就達成了我們剛開始給自己設定的需求。