深入理解Promise.all
了解es6的Promise的人應該都聽過Promise.all,而且應該是大多數的人都用過Promise.all這個方法。首先Promise.all可以將多個Promise實例包裝成一個Promise實例。
let p = Promise.all([p1, p2, p3])
Promise.all方法可以接受一個數組作為參數,數組中的每一項都是一個Promise的對象實例(如果不是,就會先調用Promise.reslove方法,將參數轉化為Promise對象實例,再進行下一步的處理)。Promise.all方法的參數不一定是數組,但是必須具有Iterator接口。
- 只有數組里每個Promise的狀態都編程了Fulfilled,Promise.all的狀態才會變成Fulefilled,然后將每一個狀態發返回的值,組裝程一個數組返回給后面的回調函數。
- 只要數組里面有一個Promise的狀態是Rejected,Promise.all的狀態就會變成Rejected。這個時候第一個被Rejected的實例的結果會傳遞給后面你的回調。
- 如果作為參數的Promise實例自身定義了catch方法,那么他被Rejected時並不會被Promise.all的catch的方法給接受。
Promise.all隊列中異步的執行順序
Promse.all在處理多個異步處理時非常有用,比如說一個頁面上需要等兩個或多個ajax的數據回來以后才正常顯示,在此之前只顯示loading圖標。但是針對於Promse.all中的多個異步的執行順序是並行的還是串行的呢?這邊我通過一個例子來證明一下:
function promise1 () {
return new Promise(resolve => {
console.log('promise1 start');
setTimeout(() => {
console.log('promise1 over');
resolve();
}, 100);
})
}
function promise2 () {
return new Promise(resolve => {
console.log('promise2 start');
setTimeout(() => {
console.log('promise2 over');
resolve();
}, 90);
})
}
Promise.all([promise1(), promise2()])
我們看上面的例子,如果說,Promise.all中的方法是串行的話,那么輸出的順序應該是 promise1 start
-> promise1 over
-> promise2 start
-> promise2 over
。但是實際的輸出的順序卻不是這樣的。具體的結果如下圖:
這也就說明,在Promise.all中的執行的順序其實應該是並行的(其實這邊說並行是有問題的)。
如何實現Promise.all
在我們都知道了Promise.all的基本原理和串並行的關系之后,我們就來動手實現一下這個Promise.all的內容。
Promise.all = function (promise) {
return new Promise((resolve, reject) => {
let index = 0
let result = []
if (promise.length === 0) {
resolve(result)
} else {
function processValue(i, data) {
result[i] = data
if (++index === promise.length) {
resolve(result)
}
}
for (let i = 0; i < promise.length; i++) {
Promise.resolve(promise[i]).then((data) => {
processValue(i, data)
}, (err) => {
reject(err)
return
})
}
}
})
}
我們來簡單的分析一下,首先我們知道,Promise.all返回的結果是一個Promise對象,那么,我們的方法也返回這樣的一個對象;接着,我們上面講到了,有關Promise.all的內部是並行執行的因此們這邊不用等待上一個任務執行完再去執行,而是可以直接將任務塞到執行隊列中,當每一個reslove之后,我們將最終的結果給reslove掉,如果其中有一個存在問題,我們就直接reject掉。因此,就形成了我們上面的內容。
Promise.all並發限制
現在我們清楚了Promse.all在處理多個異步處理時非常有用。但是如果說這個異步特別多的時候會是怎樣的結果呢。比如說我們要加載100個接口的數據,當這個100個接口的數據加載完成之后我們在給返回一個加載完成。如果我們按照Promise.all的思路去寫的話,我們會這么寫:
let urls = [
'https://www.api.com/images/getData1',
'https://www.api.com/images/getData2',
'https://www.api.com/images/getData3',
'https://www.api.com/images/getData4',
'https://www.api.com/images/getData5',
'https://www.api.com/images/getData6',
……
'https://www.api.com/images/getData100'
];
function loadData(url) {
return new Promise((resolve, reject) => {
$.get(url, result => {
resolve(result)
})
})
};
let allList = []
urls.forEach(url => allList.push(loadData(url)))
Promise.all(allList).then(() => {
console.log('success')
})
我們想象一下,如果我們一下給后台同時發送1000個請求,那么后台能夠扛得動么,對於這樣的問題我們需要怎么去解決呢,這個時候我們會想根據Promise.all的原理,將Promise.all的結果改裝成串行執行的,如下:
Promise.asyncAll = function (promise) {
return new Promise((resolve, reject) => {
let index = 0
let result = []
if (promise.length === 0) {
resolve(result)
} else {
function runPromise () {
if (index === promise.length) {
resolve(result);
} else {
Promise.resolve(promise[index]).then(data => {
result[index] = data;
++index;
runPromise()
}, err => {
reject(err)
});
}
}
runPromise()
}
})
}
這樣就不容易出現高並發的問題,但是新的問題又來了,我們這樣操作的話,其實我們的執行效率非常的低。假如說我們每個請求是1s這樣的100個請求的話所用的時間就是100s,我們要等很久。那么面對這個問題我們怎么去處理呢,其實我們可以考慮一下,一次並行請求5個,這樣的話,對服務器的壓力相對來說會減少一些,而且等待請求完成的時間也會相對應的減少一些。這個時候我們就依賴於我們上面的Promise.all的方法來進行實現:
Promise.asyncStep = function (promises) {
return new Promise((resolve, reject) => {
let index = 0
let stepCount = 5
let result = []
let count = promise.length
if (promise.length === 0) {
resolve(result)
} else {
function runPromise () {
if (index === promise.length) {
resolve(result);
} else {
stepCount = Math.min(count, stepCount)
for (let i = 0; i < stepCount; i++) {
--count;
Promise.resolve(promise[index]).then(data => {
result[index] = data;
++index;
}, err => {
reject(err)
});
}
runPromise();
}
}
runPromise()
}
})
}