JS非同步任務的並行、序列,以及二者結合

路澤宇發表於2023-10-26

讓多個非同步任務按照我們的想法執行,是開發中常見的需求。今天我們就來捋一下,如何讓多個非同步任務並行,序列,以及並行序列相結合。

 

一、並行

並行是使用最多的方式,多個相互間沒有依賴關係的非同步任務,並行執行能夠提高效率。

我們最經常用的,是Promise.all() 。

function f1() {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            console.log('1結束');
            resolve();
        }, 1000)
    });
}
function f2() {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            console.log('2結束');
            resolve();
        }, 900)
    });
}
function f3() {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            console.log('3結束');
            resolve();
        }, 800)
    });
}

let arr = [f1, f2, f3];
Promise.all(arr.map(i => i()));
// 3結束
// 2結束
// 1結束

以下幾種陣列遍歷方式,同樣可以實現並行。

// forEach遍歷
arr.forEach(item => {
    item();
});
// for迴圈
for (let i = 0; i < arr.length; i++) {
    arr[i]();
}
// for...of遍歷
for (let item of arr) {
    item();
}
// 注意,以下兩種寫法同樣是並行的 arr.forEach(async item => await item());
async
function f() { arr.forEach(async item => await item()) } f();

相比之下,Promise.all()可以確保任務都執行成功,然後再執行後續操作,這是各種遍歷無法做到的。

另外,還有一種方式也能實現並行:Promise.allSettled()。

Promise.allSettled(arr.map(i => i()));

這種方式很特別,它無法得到每個Promise物件的返回值,卻可以精確得知每個任務的成功還是失敗。如果你有這樣的需求場景,用Promise.allSettled()就很合適。

 

二、序列

我在工作中遇到過一個場景,一個有1000+元素的陣列,每個成員都是呼叫第三方介面的Promise物件。我像往常一樣得意的使用Promise.all(),等著1000多個任務瞬間完成。然而,結果卻讓我大跌眼鏡,這1000多個任務,只有一部分成功了,大部分都報錯了。不管我執行幾次,結果都是這個樣。一籌莫展之後,我才從第三方那兒得知,他們的介面是有呼叫限制的,一個介面同一時間只能並行300個。

有沒有辦法能讓它們一個接一個的執行呢?也就是序列。

nodejs koa框架的next()語法給了我啟發,它就是讓中介軟體一個接一個的執行。於是我想出了遞迴的方式:

async function serial(arr) {
    let item = arr.shift();

    await item();
    if (arr.length > 0) {
        await serial(arr);
    }
}
serial(arr);
// 1結束
// 2結束
// 3結束

其實,想讓非同步任務序列,不用這麼麻煩。以下遍歷的方式,同樣可以實現序列。

// 使用for...of
async function f() {
    for (let item of arr) {
        await item();
    }
}
f();

// 使用for迴圈
async function f() {
    for (let i = 0; i < arr.length; i++) {
        await arr[i]();
    }
}
f();

發現了沒?為什麼同樣是for迴圈,同樣是for...of,前面的寫法是並行,後面就成了序列呢?

工作中,我們一定做過這樣的嘗試,想透過遍歷,來讓多個非同步任務序列。但往往不得其法,怎麼折騰它們都還是同時執行。

後一種寫法,你可以理解為:await執行完成後,才會進入下一次迴圈。 其實,遍歷,就相當於把每一個元素,在程式碼中從上到下寫下來。當它們處於async函式中,並在每個元素前面加await,它們自然就能序列執行。否則,我們都知道,簡單的從上到下寫下來的非同步任務,它們還是並行執行的。

好了,現在程式不報錯了。但是,1000多個任務依次執行完成,足足花了十多分鐘,太慢了!有沒有辦法,又快又不觸發介面呼叫限制呢?

有,如果可以並行200個任務,完成後再開始下一輪200個......也就是,把並行和序列相結合。

 

三、並行序列結合

async function bingChuan(arr, num) {
    let items = arr.splice(0, num);
    
    await Promise.all(items.map(i => i()));
    if (arr.length > 0) {
        await bingChuan(arr, num);
    }
}
bingChuan(arr, 2);
// 2結束
// 1結束
// 3結束

好了,現在可以同時享有並行和序列的好處了!

 

本人水平非常有限,寫作主要是為了把自己學過的東西捋清楚。如有錯誤,還請指正,感激不盡。

 

相關文章