javascript 尝试了解setTimeout Promise等待的顺序

ldioqlga  于 2022-11-27  发布在  Java
关注(0)|答案(2)|浏览(178)

我试图理解代码中事件的顺序。我假设在一个点击事件之后,当Promise 2(//2),则for循环将在等待Promise 1之后继续,并且由于在等待Promise 2之前stop已被设置为true而中断。然而,它是不可预测的。如何正确地考虑这些事件在事件循环中的顺序?
第一个

nc1teljy

nc1teljy1#

Javascript是单线程的。有一个宏任务队列(setTimeout回调和点击处理程序)和另一个微任务队列(用于恢复等待异步函数/承诺的代码)。微任务总是优先于宏任务。
执行单击处理程序的宏任务只能在//1行由于使用setTimeout而挂起之后运行。
但随后,单击处理程序设置stop=true并调用setTimeout本身,导致其本身也挂起。
现在,有一个竞赛。哪个setTimeout将首先返回:是//1行的还是//2行的
如果两个setTimeouts上的延迟相同(或两者都有未指定的延迟参数),您会预期它们会以setTimeouts起始的顺序执行。不过,您无法依赖浏览器来预测setTimeout。
如果您在//2行的setTimeout调用中指定了100 ms的延迟,那么在单击后循环将更可靠地中断。但是,即使这样也不能绝对保证!
您的浏览器将自行决定延迟多长时间,并不做任何保证。
你会注意到,在Chrome中没有指定延迟时,如果你点击得慢,循环很少会中断。但是,如果你点击得快,循环中断的可能性很大。区别不在于点击的次数,而在于你是否点击得快。(注意:有时当我重复这个实验时,它确实会更频繁地坏掉。浏览器是非常不可预测的!)
此处列出了不可预测的setTimeout延迟的原因。特别是,
如果页面(或操作系统/浏览器)忙碌其他任务,则超时也可能比预期的时间晚触发
这就是为什么快速点击会导致setTimeouts延迟不同于慢速点击的原因。我猜想,由于//2中的setTimeout是响应UI事件的,浏览器会决定优先处理setTimeout,以使网页响应更快。因此,循环不会中断。但是,当您快速点击时,然后浏览器将开始延迟所有setTimeouts,并随机增加额外延迟,导致它们突然以不同的顺序执行。

cyvaqqii

cyvaqqii2#

我相信这是由于Andrew指出的最小4毫秒:https://developer.mozilla.org/en-US/docs/Web/API/setTimeout#reasons_for_delays_longer_than_specified
按照HTML标准中的规定,一旦对setTimeout的嵌套调用被调度5次,浏览器将强制执行最小超时4毫秒。
await将下面的代码捆绑到回调函数中。在for循环的上下文中,剩余的迭代包含在该回调中,满足“嵌套”setTimeout的条件。
如果将console.log(zero);更改为console.log(new Date().getMilliseconds());,您可以看到这种效果,您会看到前5个日志非常接近,而接下来的日志则相距较远。
以下是具体情况:
首先,我们建立半无限循环:

await new Promise((r1)=>setTimeout(r1)); //1
if (stop) {
   break;
}

(r)=>setTimeout(r)被立即执行,因此r1()被推到事件队列的底部。在5次迭代之后,这得到一个附加的时间戳,它是未来4 ms的时间。await将下面的代码捆绑到一个回调中,当promise解析时,该回调也将被推到事件队列的底部。
单击:

stop = true;
await new Promise((r2)=>setTimeout(r2)); //2
stop = false

stop = true(r2)=>setTimeout(r2)立即被执行,因此r2()被推到事件队列的底部。同样,await将下面的代码捆绑到回调中,当promise解析时,该回调也将被推到事件队列的底部。
因此,有六个事件正在发生:

  1. r1被推到事件队列的底部,超时时间为4 ms
  2. r1到达事件队列的顶部。如果已过去4 ms,则执行r1,并将if (stop) break;推到事件循环的底部。如果未过去4 ms,则将r1再次推到底部。
  3. if (stop) break;到达事件队列的顶部并被执行,如果循环没有中断,则再次执行事件1。
    1.用户单击,执行stop = true,并将r2推到事件队列的底部。
  4. r2到达事件队列的顶部并被执行,则stop = false被推到事件队列的底部。
  5. stop = false到达事件队列的顶部并被执行。
    您可以看到,要中断循环,事件3需要在事件4之后、事件6之前发生。在零延迟的情况下,不存在点击时事件3不在4和6之间发生的可能的事件顺序。这是由于被推到队列底部的任何东西都不会被重新推到队列底部的事实。但是,一个是零延迟,一个是4 ms延迟,这里有一个这样的顺序,不会打破循环:
  • 事件1(按下r1,超时时间为4 ms)
  • 事件2(4 ms已过,if (stop) break;已按下)
  • 事件3和1(if (stop) break;已执行,循环未中断,r1已推送,超时时间为4 ms)
  • 事件4(用户点击、stop = truer2已推送)
  • 事件2(4 ms尚未过去,再次按下r1
  • 事件5(stop = false已推送)
  • 事件2(4 ms已过,if (stop) break;已按下)
  • 事件6(已执行stop = false
  • 事件3和1(if (stop) break;已执行,循环未中断,r1已推送,超时时间为4 ms)

恭喜您,您在单线程语言中创建了争用条件。
将两个超时同步到大于4 ms * 似乎 * 可以解决此问题

let zeros = new Array(10000).fill(0);

(async () => {
    let stop = false;
    document.addEventListener('click', async ()=>{
        console.log('click');
        stop = true;
        await new Promise((r)=>setTimeout(r,5)); //2
        stop = false;
    });

    for (let zero of zeros) {
        await new Promise((r)=>setTimeout(r,5)); //1
        if (stop) {
            break;
        }
        console.log(zero);
    }
})();

然而,理论上仍然有可能使循环不被打破。以下事件顺序将允许循环继续:

  • 事件1(r1已推送,超时时间为5 ms)
  • 事件4(用户点击、stop = truer2推送,超时时间为5 ms)
  • 事件2(事件1后未经过5 ms,再次按下r1

事件1和事件4可能发生得很快,但事件2和事件5之间需要一些时间-这就是事件5在事件2之前完成的原因。

  • 事件5(从事件4起经过5 ms,stop = false被按下)
  • 事件2(从事件1起经过5 ms,if (stop) break;被按下)
  • 事件6(已执行stop = false
  • 事件3和1(if (stop) break;已执行,循环未中断,r1已推入)

我还没有能够重现它,但是嘿,让我们不要这样编码。
要实现你想做的事情非常简单,只需要使用setInterval而不是循环。这将在执行后循环回调到队列的后面,这允许其他事件在其间执行。这与whilefor循环相反,它们将在运行时阻塞任何其他执行。

let stop = false;

document.addEventListener('click', () => (stop = true));

const id = setInterval(() => {
  console.log('running');
  if (stop) {
    console.log('stopped');
    clearInterval(id);
  }
});

相关问题