JavaScript事件循环无序执行

JavaScript事件循环无序执行,javascript,settimeout,event-loop,Javascript,Settimeout,Event Loop,我尝试了一本书中的一个例子来确认JavaScript事件循环是如何工作的,下面是代码 const baz = () => console.log("baz"); const bar = () => console.log("bar"); const foo = () => { console.log("foo"); setTimeout(bar, 0); baz(); } foo(); se

我尝试了一本书中的一个例子来确认JavaScript事件循环是如何工作的,下面是代码

const baz = () => console.log("baz");
const bar = () => console.log("bar");

const foo = () => {
    console.log("foo");
    setTimeout(bar, 0);
    baz();
}
foo();
setTimeout在这里的工作方式很简单(按顺序执行),输出是

foo
baz
bar 
我不明白的是我加了一行时的顺序

const baz = () => console.log("baz");
const bar = () => console.log("bar");

const foo = () => {
    console.log("foo");
    setTimeout(bar, 0);
    baz();
}
setTimeout(baz, 0); // this somehow runs before foo() is finished
foo();
输出为

foo
baz
baz 
bar

为什么在foo()完成之前要进行第二次setTimeout漂洗?

如果将异步操作与代码的其余部分分开,您可以清楚地看到:

const baz = () => console.log("baz");
const bar = () => console.log("bar");

const foo = () => {
    console.log("foo");
    setTimeout(bar, 0);
    baz();
}
setTimeout(baz, 0); // this somehow runs before foo() is finished
foo();
如果我们提取
setTimeout
函数,我们将拥有:

const baz = () => console.log("baz");
const bar = () => console.log("bar");

const foo = () => {
    console.log("foo");
    // setTimeout(bar, 0);
    baz();
}
// setTimeout(baz, 0); // this somehow runs before foo() is finished
foo();
它记录前两行:

foo
baz
在第一个循环期间,
setTimeout
中定义的异步函数已添加到下一个循环中,因此我们必须执行它们:

setTimeout(baz, 0);
setTimeout(bar, 0);
产生下面两行日志:

baz 
bar

您需要记住,所有同步代码将首先运行,然后是异步代码。(
setTimeout
将始终对异步操作进行排队,即使超时设置为0。)

因此,考虑到这一点,事件的顺序如下:

  • 注册对
    baz()的异步调用
  • 同步输出
    foo
  • 注册对
    bar()的异步调用
  • 同步调用
    baz()
    ,输出
    baz
知道同步功能首先运行,我们首先得到
foo
,然后是
baz


然后我们的异步事件运行,依次输出
baz
bar

调用
setTimeout(baz,0)
函数,它将进入调用堆栈,然后进入事件循环中的计时器阶段,并在那里等待

调用
foo()
函数后,将
console.log(“foo”)
插入调用堆栈,
setTimeout(bar,0)
baz()
。 由于
console.log(“foo”)
是一个同步操作,它会立即执行,您可以在输出中看到
“foo”
setTimeout(条形图,0)
转到事件循环中的计时器阶段并等待。 然后执行
baz()。
执行同步操作后,调用堆栈为空,计时器等待结束,开始执行来自
设置超时的回调

setTimeout(baz,0)
->
baz
->
console.log(“baz”)
=
“baz”
在otput中

然后呢


setTimeout(bar,0)
->
bar
->
console.log(“bar”)
=
“bar”
在otput中这里是从事件循环角度的解释

您可以可视化调用堆栈,它用于跟踪在给定时间点我们在程序中的位置。当你调用一个函数时,我们把它推到堆栈上,当我们返回/完成一个函数时,我们把它从堆栈顶部弹出。最初,堆栈是空的

当您第一次运行代码时,您的主“脚本”将被推送到堆栈上,并在脚本完成执行后从堆栈中弹出:

Stack:
------
- Main()   // <-- indicates that we're in the main script
setTimeout()
启动一个web api,它在
0
ms之后将您的
baz
回调排入任务队列。
setTimeout
将其工作传递给web api后,其作业完成,因此它已完成工作并可以从堆栈中弹出:

Stack:
------
- setTimeout(baz, 0)
- Main()
Stack:
------
- Main()

Task Queue: (Front <--- Back)
baz
Stack:
------
- log("foo")
- foo()
- Main()

Task Queue: (Front <--- Back)
baz
Foo然后调用console对象的
log()
方法,该方法也被推送到堆栈上:

Stack:
------
- setTimeout(baz, 0)
- Main()
Stack:
------
- Main()

Task Queue: (Front <--- Back)
baz
Stack:
------
- log("foo")
- foo()
- Main()

Task Queue: (Front <--- Back)
baz
最后,我们到达调用
baz()
的函数
foo
的最后一行。这会将
baz()
推到调用堆栈上,然后将
log(“baz”)
推到调用堆栈的顶部,调用堆栈将记录“baz”。到目前为止,我们已经记录了“foo”和“baz”。在baz被记录之后,
log()
将从堆栈中弹出,完成后,
baz()
也将弹出堆栈

一旦
foo()
中的最后一行完成,我们将隐式返回,从堆栈中弹出
foo()
,留下
Main()
。一旦我们从foo返回,在调用
foo()
之后,我们的控制/执行将返回到主脚本。由于脚本中没有更多的函数可调用,我们从堆栈中弹出
Main()
,留下以下内容:

Stack:
------
- foo()
- Main()

Task Queue: (Front <--- Back)
baz, bar
Stack:
------
<EMPTY>

Task Queue: (Front <--- Back)
baz, bar 
现在堆栈再次为空,事件循环从队列(即:
bar
)中获取第一个任务并将其推入堆栈
bar
然后调用
log(“bar”)
,这会将
log(“bar”)
添加到堆栈中,并将日志“bar”添加到控制台中。日志记录完成后,
log()
bar()
都会从堆栈中弹出

因此,日志的输出将按以下顺序打印(请参见上面的粗体日志):


可以在事件循环和调用堆栈上找到一些好的资源。

将事件循环视为一个队列,您的代码在调用foo之前插入一些稍后要做的事情,调用foo,foo在队列中附加一些其他内容,foo结束,事件循环从队列中提取第一次提交。啊,我明白了,我现在明白了,非常感谢