Node.js - 以与注册的顺序相同的方式触发的setTimeout回调吗?

时间:2018-02-05 00:37:10

标签: javascript node.js callback event-loop

我想知道node.js是否保证"过期的执行顺序" (准备执行)通过setTimeout安排的回调。 manual似乎声称事件循环的定时器阶段具有回调的FIFO队列。

在下面的示例中考虑到这一点,我希望节点安排第一个回调,并在1秒后按照代码中指定的顺序安排剩余的两个回调。现在,当第一个回调触发时,执行"停止"持续5秒,这意味着当回调返回时,其他两个也准备好执行。

但是,当我运行示例时,输出似乎是first, third, second。奇怪的是,当第二回调的延迟时间被修改为例如2001而不是2000时,顺序如预期的那样,即first, second, third。这是设计的行为吗?

const spawnSync = require('child_process').spawnSync;

function wait(delta){
    spawnSync('sleep', [delta]);
}

setTimeout(() => {
    console.log('first');
    wait(5);
}, 2000);

wait(1);

setTimeout(() => {
    console.log('second');
}, 2000);

setTimeout(() => {
    console.log('third');
}, 4000);

2 个答案:

答案 0 :(得分:0)

来自docs

  

在该精确的毫秒数之后执行的超时间隔不能依赖于执行。这是因为阻塞或保留在事件循环上的其他执行代码将推迟执行超时。唯一的保证是超时不会比声明的超时间隔更早执行。

我认为你的例子是展示所有这些要点,更重要的是可以推迟计时器的事实。在我看来,拨打spawnSync的呼叫正在推迟定时器,足以导致重叠 - 大概是如果你减少了wait呼叫在第一个setInterval中的延迟或增加了上次setInterval的超时,您会看到更一致的行为。

答案 1 :(得分:0)

仔细检查Node中的回调调度实现后,确实看来发布的示例中second / third回调的顺序确实无法保证。

这就是Node如何在setTimeout中处理lib/timers.js回调的原因。简而言之,这些回调存储在按相应延迟时间分组的链表中。现在,如果一个回调触发,Node确定其组,标记当前时间t_now并处理组内的所有回调。对于每一个,它知道其注册时间t_reg(通过setTimeout注册时)和延迟时间delta。如果t_now - t_reg >= delta,则调用回调。问题是t_now仅针对与相同延迟时间相对应的整组回调进行一次评估。

为了说明这一点,我在调试模式(--debug脚本的./configure选项)中编译了Node,并将示例执行为:

NODE_DEBUG=timer node ./example.js

在我的机器上,我得到以下内容:

TIMER 38963: no 2000 list was found in insert, creating a new one
TIMER 38963: no 4000 list was found in insert, creating a new one
TIMER 38963: timeout callback 2000
TIMER 38963: now: 2069
TIMER 38963:    _idleStart = 64
first
TIMER 38963:    _idleStart = 1074
TIMER 38963: 2000 list wait because diff is 995
TIMER 38963: timeout callback 4000
TIMER 38963: now: 7075
TIMER 38963:    _idleStart = 1074
third
TIMER 38963: 4000 list empty
TIMER 38963: timeout callback 2000
TIMER 38963: now: 7076
TIMER 38963:    _idleStart = 1074
second
TIMER 38963: 2000 list empty

在这里,我们看到firstsecond回调分别安排在t1=64t2=1074。当first回调准备好在时间T=2069触发时,执行它需要大约5秒钟。一旦这个 执行完成后,Node继续在同一组回调中(即与相同延迟时间相关的回调),从而检查second回调。但是,它将当前时间考虑为执行first回调后的时间,而不是当它开始处理回调时的时间T(在{{1中输入listOnTimeout函数}})。在我的计算机上,由于lib/timers.js回调是在时间second注册的,1074小于2069 - 1074的延迟时间,因此2000回调不是执行但重新安排,以后延迟时间为second(但是,相对于当前时间,而不是995)。

更具体地说,由于T回调触发,我们知道first。但是,T - t1 >= deltaT - t2之间的关系无法保证。为了说明这一点,如果我删除delta来电,我会:

wait(1)

现在,TIMER 39048: no 2000 list was found in insert, creating a new one TIMER 39048: no 4000 list was found in insert, creating a new one TIMER 39048: timeout callback 2000 TIMER 39048: now: 2067 TIMER 39048: _idleStart = 66 first TIMER 39048: _idleStart = 67 second TIMER 39048: 2000 list empty TIMER 39048: timeout callback 4000 TIMER 39048: now: 7080 TIMER 39048: _idleStart = 67 third TIMER 39048: 4000 list empty 回调计划在second,即t2=67个时间晚于2回调。现在当first在时间first触发时,Node会处理与T=2067的延迟时间相关联的整组回调,如上所述,从而进行2000回调 - 在这种情况下,second完全等于T-t2,因此2000也“幸运地”被解雇了。但是,如果second回调的调度仅仅second单位延迟(由于例如在调用1之前有一些无关的函数调用),setTimeout回调会更快地触发