JavaScript事件循环乱序执行
JavaScript Event Loop out of order execution
我正在尝试书中的一个示例来确认 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(bar, 0)
转到事件循环中的计时器阶段并等待。
然后执行 baz()
函数,该函数依次启动 console.log("baz")
,您会在输出中看到 "baz"
。
执行同步操作后,调用栈为空,定时器等待结束,开始执行setTimeouts
.
回调
setTimeout(baz, 0)
-> baz
-> console.log("baz")
= "baz"
在 otput
之后
setTimeout(bar, 0)
-> bar
-> console.log("bar")
= "bar"
在 otput
这里从事件循环的角度进行解释。
您可以可视化调用堆栈,它用于跟踪我们在给定时间点在程序中的位置。当您调用一个函数时,我们将其压入堆栈,而当我们 return/complete 一个函数时,我们将其从堆栈顶部弹出。最初,堆栈是空的。
当您第一次 运行 您的代码时,您的主要“脚本”将被压入堆栈,并在脚本执行完毕后从堆栈弹出:
Stack:
------
- Main() // <-- indicates that we're in the main script
然后我们定义了几个函数 baz
、bar
和 foo
,并最终到达我们的第一个函数调用 setTimeout(baz, 0)
,因此,我们将其推送到堆栈:
Stack:
------
- setTimeout(baz, 0)
- Main()
setTimeout()
启动 web-api,在 0
毫秒后将您的 baz
回调排入 任务队列 .在 setTimeout
将它的工作传递给 web-api 之后,它的工作就完成了,因此它也完成了它的工作并且可以从堆栈中弹出:
Stack:
------
- Main()
Task Queue: (Front <--- Back)
baz
事件循环的工作是从任务队列中取出任务,并在堆栈空时将它们压入堆栈。目前,堆栈不为空,因为我们仍在主脚本中,所以 baz()
尚未执行。我们遇到的下一个函数调用是 foo()
,因此我们将其压入堆栈:
Stack:
------
- foo()
- Main()
Task Queue: (Front <--- Back)
baz
Foo 然后调用控制台对象的 log()
方法,该方法也被压入堆栈:
Stack:
------
- log("foo")
- foo()
- Main()
Task Queue: (Front <--- Back)
baz
这会记录 "foo"
,并且 log()
在完成工作后从堆栈中弹出。然后我们继续单步执行函数 foo。我们现在遇到了对 setTimeout(bar, 0);
的函数调用。这与第一个函数调用非常相似,将 setTimeout(bar, 0)
压入堆栈。这会衍生出一个 web-api,它将 bar
添加到任务队列中。 setTimeout(bar, 0)
在将其工作移交给 web-api 后也已完成,因此它也会从堆栈中弹出(有关这些步骤,请参见第二个和第三个 ascii-diagrams),留给我们的是:
Stack:
------
- foo()
- Main()
Task Queue: (Front <--- Back)
baz, bar
最后,我们到达调用 baz()
的函数 foo
的最后一行。这会将 baz()
推送到调用堆栈,然后将 log("baz")
推送到调用堆栈的顶部, 记录“baz”。到目前为止,我们已经记录了“foo”,然后是“baz”。在记录 baz 后,log()
被弹出堆栈,baz()
也被弹出。
一旦 foo()
中的最后一行完成,我们隐含地 return,将 foo()
弹出堆栈,留下 Main()
。一旦我们从 foo returned,我们的 control/execution 在 foo()
被调用后被 returned 回到主脚本。由于我们的脚本中没有更多的函数可以调用,我们从堆栈中弹出 Main()
,留下:
Stack:
------
<EMPTY>
Task Queue: (Front <--- Back)
baz, bar
现在堆栈为空,事件循环可以进入并处理任务队列中的 baz
和 bar
。首先是从队列中取出 baz
并将其压入堆栈,然后调用 log("baz")
,将 log
压入堆栈,然后 记录“baz”。日志完成后,log
和 baz
从堆栈中弹出,再次留空:
Stack:
------
<EMPTY>
Task Queue: (Front <--- Back)
bar
现在堆栈再次为空,事件循环从队列中取出第一个任务(即:bar
)并压入堆栈。 bar
然后调用 log("bar")
,它将 log("bar")
添加到堆栈,以及 logs "bar" 到控制台。记录完成后,log()
和 bar()
都从堆栈中弹出。
因此,您的日志输出按以下顺序打印(请参阅上面的粗体日志):
"foo"
"baz"
"baz"
"bar"
我正在尝试书中的一个示例来确认 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(bar, 0)
转到事件循环中的计时器阶段并等待。
然后执行 baz()
函数,该函数依次启动 console.log("baz")
,您会在输出中看到 "baz"
。
执行同步操作后,调用栈为空,定时器等待结束,开始执行setTimeouts
.
setTimeout(baz, 0)
-> baz
-> console.log("baz")
= "baz"
在 otput
之后
setTimeout(bar, 0)
-> bar
-> console.log("bar")
= "bar"
在 otput
这里从事件循环的角度进行解释。
您可以可视化调用堆栈,它用于跟踪我们在给定时间点在程序中的位置。当您调用一个函数时,我们将其压入堆栈,而当我们 return/complete 一个函数时,我们将其从堆栈顶部弹出。最初,堆栈是空的。
当您第一次 运行 您的代码时,您的主要“脚本”将被压入堆栈,并在脚本执行完毕后从堆栈弹出:
Stack:
------
- Main() // <-- indicates that we're in the main script
然后我们定义了几个函数 baz
、bar
和 foo
,并最终到达我们的第一个函数调用 setTimeout(baz, 0)
,因此,我们将其推送到堆栈:
Stack:
------
- setTimeout(baz, 0)
- Main()
setTimeout()
启动 web-api,在 0
毫秒后将您的 baz
回调排入 任务队列 .在 setTimeout
将它的工作传递给 web-api 之后,它的工作就完成了,因此它也完成了它的工作并且可以从堆栈中弹出:
Stack:
------
- Main()
Task Queue: (Front <--- Back)
baz
事件循环的工作是从任务队列中取出任务,并在堆栈空时将它们压入堆栈。目前,堆栈不为空,因为我们仍在主脚本中,所以 baz()
尚未执行。我们遇到的下一个函数调用是 foo()
,因此我们将其压入堆栈:
Stack:
------
- foo()
- Main()
Task Queue: (Front <--- Back)
baz
Foo 然后调用控制台对象的 log()
方法,该方法也被压入堆栈:
Stack:
------
- log("foo")
- foo()
- Main()
Task Queue: (Front <--- Back)
baz
这会记录 "foo"
,并且 log()
在完成工作后从堆栈中弹出。然后我们继续单步执行函数 foo。我们现在遇到了对 setTimeout(bar, 0);
的函数调用。这与第一个函数调用非常相似,将 setTimeout(bar, 0)
压入堆栈。这会衍生出一个 web-api,它将 bar
添加到任务队列中。 setTimeout(bar, 0)
在将其工作移交给 web-api 后也已完成,因此它也会从堆栈中弹出(有关这些步骤,请参见第二个和第三个 ascii-diagrams),留给我们的是:
Stack:
------
- foo()
- Main()
Task Queue: (Front <--- Back)
baz, bar
最后,我们到达调用 baz()
的函数 foo
的最后一行。这会将 baz()
推送到调用堆栈,然后将 log("baz")
推送到调用堆栈的顶部, 记录“baz”。到目前为止,我们已经记录了“foo”,然后是“baz”。在记录 baz 后,log()
被弹出堆栈,baz()
也被弹出。
一旦 foo()
中的最后一行完成,我们隐含地 return,将 foo()
弹出堆栈,留下 Main()
。一旦我们从 foo returned,我们的 control/execution 在 foo()
被调用后被 returned 回到主脚本。由于我们的脚本中没有更多的函数可以调用,我们从堆栈中弹出 Main()
,留下:
Stack:
------
<EMPTY>
Task Queue: (Front <--- Back)
baz, bar
现在堆栈为空,事件循环可以进入并处理任务队列中的 baz
和 bar
。首先是从队列中取出 baz
并将其压入堆栈,然后调用 log("baz")
,将 log
压入堆栈,然后 记录“baz”。日志完成后,log
和 baz
从堆栈中弹出,再次留空:
Stack:
------
<EMPTY>
Task Queue: (Front <--- Back)
bar
现在堆栈再次为空,事件循环从队列中取出第一个任务(即:bar
)并压入堆栈。 bar
然后调用 log("bar")
,它将 log("bar")
添加到堆栈,以及 logs "bar" 到控制台。记录完成后,log()
和 bar()
都从堆栈中弹出。
因此,您的日志输出按以下顺序打印(请参阅上面的粗体日志):
"foo"
"baz"
"baz"
"bar"