請求多個併發執行的一些問題及解決
最近在寫一個Node.js程式,功能是下載頁面上的資源,首先拿到頁面資源連結列表,如:
[
'https://xxx.com/img/logo.jpg',
'https://xxx.com/img/bg.jpg',
'https://xxx.com/css/main.css',
'https://xxx.com/css/animate.css',
'https://xxx.com/js/jquery.js',
'https://xxx.com/js/form.js',
...
]
要求是資源並行下載,所有資源下載結束後通知,收集錯誤的下載連結。
如果是傳統做法是遍歷陣列傳送請求,宣告一個變數記錄請求數,不管成功或失敗,結束後都給這個變數+1,並且呼叫一個函式,這個函式判斷當前變數是否等於陣列長度,相等則表示所有請求已經完成。
// pseudo code
var count = 0
var errs = []
var data = [...]
function request(url) {
ajax({url: url})
.success(function () {
count++
callback()
})
.fail(function () {
count++
errs.push(...)
callback()
})
}
function callback() {
if (count === data.length) {
console.log('done!')
}
}
data.forEach(request)
因為請求是非同步的,我們也無法確定每個請求花費的時間,所以只能在回撥裡處理。現在我們有了Promise,async-await,支援同步的寫法,那可以怎麼寫呢?
我們用setTimeout來模擬請求,資料data = [500, 400, 300, 200, 100]既是每個請求返回的資料也是每個請求所需的時間。
如果是繼發請求(一個請求結束後再請求後一個),那麼應該是按順序列印,理論上所有請求的總時間等於每個請求所花時間之和,約等於1500ms;如果是併發請求(假設請求數不會太多,不超過限制),順序是按時間從小到大列印,理論上所有請求的總時間等於最長的那個時間,約等於500ms。
首先先看下怎麼並行請求和請求結束確定
// 模擬請求
function request(param) {
return new Promise(resolve => {
setTimeout(() => {
console.log(param)
resolve()
}, param)
})
}
const items = [500, 400, 300, 200, 100]
✘ 直接for迴圈
(() => {
for (let item of items) {
request(item)
}
console.log('end')
})()
// 輸出:end, 100, 200, 300, 400, 500
上面的輸出可以看出,請求是並行的,但是無法確定什麼結束
✘ for迴圈,使用async-await
(async () => {
for (let item of items) {
await request(item)
}
console.log('end')
})()
// 輸出:500, 400, 300, 200, 100, end
上面的程式碼可以看出,雖然確定了結束,但請求是繼發的
✔ 使用Promise.all
(() => {
Promise.all(items.map(request)).then(res => {
console.log('end')
})
})()
// 輸出:100, 200, 300, 400, 500, end
上面的程式碼可以看出,請求是併發的,並且在所有請求結束後列印end,滿足條件
我們不能保證所有的請求都是正常的,接下來看看當有請求出錯時怎麼處理,假設200的請求出錯
function request(param) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (param === 200) {
// console.log(param, ' failed')
return reject({
status: 'error',
data: param
})
}
// console.log(param, ' success')
resolve({
status: 'success',
data: param
})
}, param)
})
}
const items = [500, 400, 300, 200, 100]
Promise有catch方法捕獲錯誤,最近新增的finally方法能在最後執行
(() => {
Promise.all(items.map(request))
.then(res => {
console.log(res)
})
.catch (err => {
console.log(err)
})
.finally(res => {
console.log('end', res)
})
})()
// 輸出 {status: "error", data: 200}, end, undefined
上面的輸出可以看出,如果有錯誤,則不會進入then,而是進入catch,然後進入finally,但是finally不接受引數,只告訴你結束了。如果把上面模擬請求的console.log(...)註釋去掉,還會發現finally是在catch結束後就執行了,而200後面的請求還未結束。
接下來我們改造下模擬請求,在請求出錯後就catch錯誤
function request(param) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (param === 200) {
// console.log(param, ' failed')
return reject({
status: 'error',
data: param
})
}
// console.log(param, ' success')
resolve({
status: 'success',
data: param
})
}, param)
}).catch(err => err)
}
(() => {
Promise.all(items.map(request))
.then(res => {
console.log(res, 'end')
})
})()
// 輸出 [{…}, {…}, {…}, {stauts: 'error', data: 200}, {…}], end
這樣就可以在then中拿到全部的結果了,如果要用for迴圈的話也是可以的
(async () => {
const temp = []
// 這個for迴圈的作用和上面的map類似
for (let item of items) {
temp.push(request(item))
}
const result = []
for (let t of temp) {
result.push(await t)
}
console.log(result, 'end')
})()
// 輸出與上面一致
第一個for迴圈保證併發請求,儲存了Promise,第二個迴圈加入await保證按順序執行。