今天開發中碰到一個問題,卡了挺久的記錄一下。
標題其實正確表達為:
js-while循環中不要使用定時函數中的回調函數中對循環條件的自變量進行自增或者自減,否則會導致進程假死。
首先
let i = 0;
while(i < 10){
i++;
console.log(i)
}
首先這個沒問題,正常打印1,2....10;
let i = 0;
let timer;
while(i < 10){
if(timer){
clearTimeout(timer);
}
timer = setTimeout(()=>{
console.log(i)
i++;
},100
)
}
這段代碼在瀏覽器端或者node端都會導致進程假死,
藉助文言一心3.5大模型,查詢了以下理念供參考:
這段代碼存在幾個問題,它不會按預期工作,因為 while 循環會立即執行,不會等待 setTimeout 的回調。以下是代碼的問題和解釋:
while 循環的阻塞性質:這是因為 while 循環會阻塞 JavaScript 的執行,直到循環條件不再滿足。而 setTimeout 雖然被設計為異步執行,但在 while 循環內部使用時,由於循環的阻塞,setTimeout 實際上可能無法按預期工作,或者即使工作,其效果也會被循環的阻塞所掩蓋。
i 的閉包問題:由於所有的 setTimeout 回調都引用同一個 i 變量,當它們觸發時,i 的值已經是 10 了(因為 while 循環已經完成了)。因此,你會在控制枱上看到 10 被打印了 10 次,而不是從 0 到 9 的數字。
結論1:儘量不要在while循環中使用setTimeout回調函數中進行while條件的自增或者自減,否則會導致進程假死!
for循環由於語句存在自增或自減機制是可以配合定時函數使用的,所以就不存在進程假死。
如:
for(var i=0;i<10;i++){
setTimeout(()=>{
console.log(i)
},100)
}
//10 次 10
let a = 0;
for(var i=0;i<10;i++){
setTimeout(()=>{
a++;console.log(a)
},100)
}
//1,2,....10
最後來一道BT的面試題
function api() {
return new Promise< null | string >(resolve => {
setTimeout(() => {resolve(Math.random() > .9 ? 'ok' : null)}, 300)
})
}
// 完成下面方法(類似案例:後台登錄輪詢檢查二維碼是否掃碼成功)
async function checkAvailable() {
// 循環調用api檢查data直到data等於ok,打印ok(建議使用async await 語法)
// 最多檢查10次,超時打印timeout
// 最多等待10秒,超時打印timeout
// 每次api檢查時間間隔gap 100ms
// todo 編寫代碼
}
藉助文心一言秒出.......其中註釋方法一是文心一言給你的答案。
function api() {
return new Promise< null | string >(resolve => {
setTimeout(() => {resolve(Math.random() > .9 ? 'ok' : null)}, 300)
})
}
async function checkAvailable() {
const MAX_CHECKS = 10; // 最多檢查10次
const MAX_WAIT_TIME = 10000; // 最多等待10秒
const CHECK_INTERVAL = 100; // 每次檢查間隔100ms
let checks = 0; // 當前的檢查次數
let elapsedTime = 0; // 已經過去的時間
let startTime = Date.now(); // 記錄開始時間
console.time('test-wap');
while (checks < MAX_CHECKS && elapsedTime < MAX_WAIT_TIME ) { // 檢查次數未達到上限且等待時間未達到上限
console.time('test-in');
const data = await api(); // 調用api並等待結果
if (data === 'ok') {
console.log('ok');
return; // 一旦獲取到'ok',立即返回
}
// 如果沒有獲取到'ok',則等待一段時間再檢查
//方法一 等到此次檢查結果異步任務走完,才回到主線程執行同步任務。4.176s
await new Promise(resolve => setTimeout(resolve, CHECK_INTERVAL));
checks++; // 檢查次數加1
elapsedTime = Date.now() - startTime; // 更新已經過去的時間
console.log(checks)
//方法二 多執行一次函數 每執行一次佔用調用棧內存。 3.410s
// setTimeout(()=>{ //當checks = 9時,多執行一次; 因為它是異步任務並不會阻塞 主線程繼續循環,故而當他執行完畢以後
// // 又回到主線程,此時checks已經等於10了,所以需要判斷一下
// checks++; // 檢查次數加1
// elapsedTime = Date.now() - startTime; // 更新已經過去的時間
// console.log(checks)
// }, CHECK_INTERVAL)
console.timeEnd('test-in');
}
console.log('timeout',checks,elapsedTime);
console.timeEnd('test-wap')
// 如果達到最大檢查次數或最大等待時間,則打印'timeout'
}
// 調用checkAvailable函數
checkAvailable();
這裏注意一下我寫的方法二,也能達到預期效果,但是多執行一次,這個問題又和我上面的提的結論1前半句相沖突,這個不會卡死!後來想了想是得益於之前請求API佔用了超過100ms時間,所以進程不會卡死。得出結論2:還是不要在while循環中使用setTimeout回調函數中進行while條件的自增或者自減!保不齊那一天就出現大問題,還要排查半天!