头图

今天开发中碰到一个问题,卡了挺久的记录一下。
标题其实正确表达为:
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条件的自增或者自减!保不齐那一天就出现大问题,还要排查半天!


阳哥
14 声望0 粉丝

一个code爱好者,一个户外运动的爱好者,一个喜欢音乐的爱好者。