Node.js 數組 forEach 同步處理上下文語句

習慣了C語言系的思維方式,剛接觸Node.js,它的異步處理讓我頭大。
寫代碼遇到這麼一個場景,需要循環對一個數組中的元素進行處理,全部處理完成後再執行一個last操作。但是JS的異步特性會使這個last語句先執行,所以花點時間研究研究forEach。
Talk is cheap. Show me the code.

forEach 用法

forEach用於對數組結構進行遍歷,看到有人說forEach底層是用for實現的,沒深究,起碼效果上看是一樣的。forEach的回調函數3個參數分別是:值、序號和原數組。序號從0開始。

(() => {
    let arr = [2, 3, 1];
    arr.forEach(function (value, index, array) {
        console.log(value);
        console.log(index);
        console.log(array);
        console.log('-----');
    });
})();

Output

2
0
[ 2, 3, 1 ]
-----
3
1
[ 2, 3, 1 ]
-----
1
2
[ 2, 3, 1 ]
-----

從結果上看forEach多次循環之間是同步的,也就是說都是按順序執行的。但是一想到它是JS就感覺不可能同步的。。可以驗證一下。

forEach 異步處理多次循環

這次在forEach加個定時任務,每次循環操作都延時value相關的時間,模擬比較耗時的操作。

(() => {
    let arr = [2, 3, 1];
    arr.forEach(function (value, index, array) {
        setTimeout(function () {
            console.log(value);
        }, value*100);
    });
})();

Output

1
2
3

從結果可以看出耗時最短的任務先完成,每次循環的任務並不是按循環的先後順序執行的,也就是說異步處理多次循環。

forEach 上下文也是異步執行

回到開始說到的問題了,且不管多次循環是不是按順序執行,我需要forEach中的所有任務都完成後執行一條數據來通知我任務全部完成了。

(() => {
    let arr = [2, 3, 1];
    arr.forEach(function (value, index, array) {
        setTimeout(function () {
            console.log(value);
        }, value*100);
    });
    console.log('All the work is done');
})();

Output

All the work is done
1
2
3

從結果來看,上下文的語句也不是同步的,forEach循環中的任務沒有完成就通知所有任務都完成了,顯然不符合預期。
針對這個問題看了好多個博客,都沒有找到合適的解決方法,最後只能想到用Promise.all來勉強實現這個功能。

Promise.all 實現 forEach 上下文語句同步處理

把上面的代碼改成Promise.all的結構。每個循環中執行結束調用resolve(),我們知道Promise.all的then函數,只有所有的Promise都執行完成纔會觸發,這樣好像能滿足我們的需求。

(() => {
    let arr = [2, 3, 1];
    let proArr = [];
    arr.forEach(function (value, index) {
        proArr[index] = new Promise(function (resolve) {
            setTimeout(function () {
                console.log(value);
                resolve();
            }, value*100);
        });
    });

    Promise.all(proArr).then(()=>{
        console.log('All the work is done');
    })
})();

Output

1
2
3
All the work is done

從結果來看,滿足了我們的需求。

可能還存在的問題

想到JS異步特性,突然發現可能這個方法還存在個問題。
這裏每次 forEach 剛進入就對 Promise 數組進行了賦值操作,這個操作時間應該非常短,循環3次都賦值完成後才調用最後的Promise.all語句。
但是如果這個數組非常大,這個循環賦值的操作非常耗時間的話,假如只完成了一半的賦值操作,那麼執行最後這個 Promise.all 的時候傳入的 Promise 數組可能並不是包含所有 Promise 的數組。
這樣的話 Promise.all 等待的就只有一半的操作,Promise.all 等待的時候,這個數組後面被賦值的 Promise 不知道會不會被等待。
剛接觸JS不明白實現機制,只能實驗來驗證一下是否存在這個問題。接下來用把這個數組弄大一些,請原諒我用最傻瓜式的方式搞大它。

(() => {
    let arr = [2, 3, 1, 2, 3, 1, 2, 3, 1, 2];   // 10
    arr= arr.concat(arr);   // 2^1 * 10
    arr= arr.concat(arr);   // 2^2 * 10
    arr= arr.concat(arr);   // 2^3
    arr= arr.concat(arr);   // 2^4
    arr= arr.concat(arr);   // 2^5
    arr= arr.concat(arr);
    arr= arr.concat(arr);
    arr= arr.concat(arr);
    arr= arr.concat(arr);
    arr= arr.concat(arr);   // 2^10
    arr= arr.concat(arr);
    arr= arr.concat(arr);
    arr= arr.concat(arr);
    arr= arr.concat(arr);
    arr= arr.concat(arr);   // 2^15
    arr= arr.concat(arr);
    arr= arr.concat(arr);	// 2^17 * 10
//  arr= arr.concat(arr);   // 2^18 * 10

    console.log(arr.length);
    let proArr = [];
    arr.forEach(function (value, index) {
        proArr[index] = new Promise(function (resolve) {
            setTimeout(function () {
                console.log(value);
                resolve();
            }, value*100);
        });
    });

    Promise.all(proArr).then(()=>{
        console.log('All the work is done');
        console.log(arr.length);
    }).catch(function (err) {
        console.log(err);
    })
})();

經過測試在我這個電腦上當數組長度爲2^18 * 10的時候,Promise報錯 RangeError: Too many elements passed to Promise.all
當數組長度爲2^17 * 10 即2621440的時候,會正常運行。測試了幾次,最後的執行命令輸出的All the work is done始終在最後輸出(因爲終端緩衝區太小,所以使用node xx.js > log.txt重定向的方式把輸出結果重定向到文件查看)。當然應用中也不會有這麼大的數組,從結果看的話,就是實際應用中不存在上面考慮可能出現的問題。
也就是說可以用 Promise.all 實現 forEach 上下文語句同步處理。
如果有對JS理解透徹的朋友看出了上文的問題,歡迎指教。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章