壹 ❀ 引
在一個思路搞定三道Promise並發編程題,手摸手教你實現一個Promise限制器一文中,我們在文章結尾留了一個疑問,關於第三題的實現能否解決當每次調用時間都不相等的情況(比如第二次調用要早於第一次調用結束),那么最終得到的結果順序還能與參數順序保持一致問題?在分享我踩坑過程中其實已經證明是可以滿足這種場景的,但為什么呢?
我們可以嘗試運行下面代碼,你會發現盡管輸出順序不對,但每次index與value都是正確的配隊關系:
const time = [1, 3, 4, 2, 1];
// 假設請求API為
function request(params) {
return new Promise((resolve, reject) => {
setTimeout(() => resolve(params), time[Math.floor(Math.random() * 5)] * 1000);
});
}
// 最多處理3個請求的調度器
function Scheduler(list = [], limit = 3) {
let count = 0;
// 用於統計成功的次數
let resLength = 0;
// 淺拷貝一份,原數據的length我們還有用
const pending = [...list];
const resList = [];
// 一定得返回一個promise
return new Promise((resolve, reject) => {
const run = () => {
if (!pending.length || count >= limit) return;
count++;
const index = list.length - pending.length;
const params = pending.shift();
request(params)
.then((res) => {
console.log('當前index為:', index, '當前結果為:', res);
count--;
resLength++;
// 按index來保存結果
resList[index] = res;
// 全部成功了嗎?沒有就繼續請求,否則resolve(resList)跳出遞歸;
resLength === list.length ? resolve(resList) : run();
})
.catch(reject) // 有一個失敗就直接失敗
};
// 遍歷,模擬前兩次依次調用的動作,然后在run內部控制如何執行
list.forEach(() => run());
})
}
Scheduler([1, 2, 3, 4, 5]).then((res) => console.log('最終結果為:', res)); // 1 2 3 4 5
可以毫不誇張的說,上述代碼已經算是一個滿足了並發限制器功能的Promise.all了,只要我們去除掉限制部分代碼,稍加修改就能分別得到Promise.all與Promise.race的實現,但在改寫之前我們還是先解釋為什么盡管執行順序不同,為什么結果與參數仍是對應關系的問題,而且我覺得也只剩下這一個稍微有點饒的疑惑點了。
貳 ❀ 執行上下文與閉包
還是模擬下上述代碼的執行過程,當forEach遍歷調用run時,可以確定的是,如下代碼絕對是同步執行完成的,且5次都是同步跑完:
// 獲得當前的index
const index = list.length - pending.length;
// 獲取當前請求需要的參數
const params = pending.shift();
異步的是request(),你什么時候能執行我不關系,反正一開始我已經把你執行需要的參數成對的給你准備好了。有同學的疑問可能就在於,我也知道這些參數一開始是成對的,那Promise執行順序被打亂之后,后執行的Promise又怎么知道之前的index是多少呢,這是怎么對應上的?
問題又回到了老生常談的執行上下文與閉包問題。我們知道代碼在執行前都要經歷執行上下文創建階段與執行階段,而一個函數的執行上下文在它創建時就已經決定了,而不是執行時,這也是典型的靜態作用域的概念,比如:
const a = 1;
const fn = () => {
console.log(a);
};
(() => {
const a = 2;
fn();// ???
})();
以上代碼fn執行時輸出1,這是因為fn的執行上下文在創建時決定,而不是執行時,所以不管你在哪調用它,它能訪問的永遠是同出一個作用域下的const a = 1,這里就當簡單復習下靜態作用域的概念。
回到上文我們實現的代碼,我們知道request().then()這個調用行為是同步的,異步的是requset內部修改狀態的代碼,以及狀態修改完成后才能執行的.then()所注冊的回調函數,注意.then()注冊回調的行為是同步的,這一點你一定要搞清楚。
也就是說,在五次同步的run()調用過程中,index與params在不斷的同步生成,.then()也在不斷的同步注冊回調任務。
還記得javascript中什么是閉包嗎?所謂閉包,就是能訪問外部函數作用域中自由變量的函數,而此時外部函數很明顯就是new Promise(fn)的fn,內部函數就是.then()注冊的回調函數,自由變量自然就是上面同步生成的index了,而閉包的一大特性就是,即便外部上下文已經銷毀,它依舊能訪問到當時創建它的執行上下文,以及上下文中的那些自由變量(靜態作用域的魅力)。
因此即便run()在不斷的執行與銷毀,.then()在注冊callback時這些回調已經自帶了它們后續要執行的上下文,這就像人能在地球生活,是因為地球這個上下文提供了空氣,水等物質,而宇航員離開了地球依舊能生存,是因為他們自帶了氧氣等生活物質,即使他們已不在地球這個上下文了。
假設我們斷點查看任意一個Promise執行,你會發現每次執行時都有一個closure作用域,這就是閉包的英文單詞:
若你對閉包以及執行上下文有一定疑惑,可以閱讀博主這兩篇文章:
一篇文章看懂JS閉包,都要2020年了,你怎么能還不懂閉包?
叄 ❀ 改寫實現Promise.all
好了,解釋完結果與參數的對應關系后,我們直接改寫上述代碼,得到我們的PromiseAll,它滿足2個特性:
- 只有所有
Promise全部resolve時才會resolve,且結果順序與參數保持一致。 - 任意一個失敗時直接
reject。
function PromiseAll(promiseList = []) {
// 用於統計成功的次數
let resLength = 0;
// 淺拷貝一份,原數據的length我們還有用
const pending = [...promiseList];
const resList = [];
// 一定得返回一個promise
return new Promise((resolve, reject) => {
const run = () => {
if (!pending.length) return;
const index = promiseList.length - pending.length;
const promise = pending.shift();
promise.then((res) => {
resLength++;
// 按index來保存結果
resList[index] = res;
// 全部成功了嗎?沒有就繼續請求,否則resolve(resList)跳出遞歸;
resLength === promiseList.length ? resolve(resList) : run();
})
.catch(reject) // 有一個失敗就直接失敗
};
// 遍歷,模擬前兩次依次調用的動作,然后在run內部控制如何執行
promiseList.forEach(() => run());
})
}
執行如下代碼,你會發現結果完全符合預期:
const P1 = new Promise((resolve, reject) => {
setTimeout(() => resolve(1), 3000)
});
const P2 = new Promise((resolve, reject) => {
setTimeout(() => resolve(2), 1000)
});
const P3 = new Promise((resolve, reject) => {
setTimeout(() => resolve(3), 2000)
});
PromiseAll([P1, P2, P3]).then((res) => console.log('最終結果為:', res)); // 1 2 3 4 5
假設你將上述三個Promise中任意一個的狀態改為reject,最終Promise也只會得到失敗的結果,而上述的改寫,我們還真的只是去除了限制器的代碼,理解起來也非常簡單。
肆 ❀ 改寫實現Promise.race
race顧名思義就是賽跑,多個Promise第一個執行完狀態是啥就是啥,所以針對上面的代碼,我們又只需要刪除掉resLength === promiseList.length以及遞歸的相關邏輯即可,直接上代碼:
function PromiseRace(promiseList = []) {
// 一定得返回一個promise
return new Promise((resolve, reject) => {
const run = (p) => {
p.then((res) => {
resolve(res);
})
.catch(reject) // 有一個失敗就直接失敗
};
// 遍歷,模擬前兩次依次調用的動作,然后在run內部控制如何執行
promiseList.forEach((p) => run(p));
})
}
再運行上面的例子,同樣符合預期。
伍 ❀ 總
其實從上篇的文章的題三,到后來的all race的實現,你會發現難度反而是遞減的,所以如果你對於這篇文章存在疑慮,我還是建議閱讀下前兩篇文章:
因兩道Promise執行題讓我產生自我懷疑,從零手寫Promise加深原理理解
一個思路搞定三道Promise並發編程題,手摸手教你實現一個Promise限制器
建議按順序閱讀這三篇文章,我想你對於Promise的理解以及手寫,一定會上升一個高度,那么到這里本文結束。
