Nodejs 數組的隊列以及forEach的應用詳解
本文主要記錄瞭在Nodejs開發過程中遇到過的由數組特性引起的問題及解決方式,以及對數組的靈活應用。
本文代碼測試結果均基於node v6.9.5
數組與隊列
利用數組對象方法push/shift可實現隊列先進先出特性,例如:
>a=[] [] >a.push(2.3.4) 3 >a.push(2) 3 >a [2.3.4.2] >a.shift() 2 >a >[3.4.2]
數組與forEach
對數組的刪除操作有兩種常見方式:delete和使用splice方法,需要明確他們的區別。
操作/方法 | 說明 |
---|---|
splice | 刪除並返回指定的數組元素,數組本身長度會改變;但不會free元素對象 |
delete | 刪除(free)元素對象,數組元素不變,值變為undefined |
如果要從數組中徹底刪除某個元素,使用splice即可:
> a=[1,2,3] [ 1, 2, 3 ] > a.splice(1,1) [ 2 ] > a [ 1, 3 ] > a.length 2 > a.forEach(function(item, index){console.info("index[", index,"]:", item)}); index[ 0 ]: 1 index[ 1 ]: 3 undefined >
那麼,當使用delete刪除某個元素對象後,此時執行forEach的效果是什麼?
forEach對含空元素數組處理機制
測試結果如下
> a=[1,2,3] [ 1, 2, 3 ] > delete a[1] true > a [ 1, , 3 ] > a.length 3 > a.forEach(function(item, index){console.info("index[", index,"]:", item)}); index[ 0 ]: 1 index[ 2 ]: 3 undefined
從測試結果來看,forEach並不會遍歷到值為undefined的哪一項。這在實際應用中如何判斷forEach是否結束是一大挑戰。
解決配合forEach的異步特性應用,可為數組添加prototype來自行管理設置有效數據;
效果如下:
> a=[1,2,3] [ 1, 2, 3 ] > a.validnum=3 3 > delete a[2] true > a.validnum=2 2 > a [ 1, 2, , validnum: 2 ] > a.length 3 > a.validnum 2 > a.forEach(function(item, index){console.info("index[", index,"]:", item)}); index[ 0 ]: 1 index[ 1 ]: 2 undefined >
補充: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 上下文語句同步處理。
以上為個人經驗,希望能給大傢一個參考,也希望大傢多多支持WalkonNet。如有錯誤或未考慮完全的地方,望不吝賜教。
推薦閱讀:
- JavaScript中forEach的錯誤用法匯總
- 梳理總結25JavaScript數組操作方法實例
- 字節飛書面試promise.all實現示例
- JS promise 的回調和 setTimeout 的回調到底誰先執行
- 一篇文章讓你搞清楚JavaScript事件循環