回答 1 和 3
事件队列和调用栈有很大的区别。事实上,他们几乎没有任何共同点。
调用堆栈(简单概述):
当您执行一个函数时,它所使用的所有内容都被称为在堆栈上,这与您在此处所指的调用堆栈相同。非常简化,它是功能执行的临时内存。或者换句话说
function foo() {
console.log("-> start [foo]");
console.log("<- end [foo]");
}
foo();
当调用它时,它会被赋予一个小沙盒,以便与 在堆栈上一起玩。当函数结束时,使用的临时内存将被擦除并可供其他事物使用。因此,所使用的资源(除非在某个地方提供给系统)只会持续到函数持续的时间。
现在,如果你有嵌套函数
function foo() {
console.log("-> start [foo]");
console.log("<- end [foo]");
}
function bar() {
console.log("-> start [bar]");
foo()
console.log("<- end [bar]");
}
bar();
调用函数时会发生以下情况:
-
bar 被执行 - 在堆栈上为其分配内存。
-
bar 打印“开始”
-
foo 被执行 - 在堆栈上为其分配内存。 注意! bar 仍在运行,它的内存还在。
-
foo 打印“开始”
-
foo 打印“结束”
-
foo 完成执行并从堆栈中清除其内存。
-
bar 打印“结束”
-
bar 完成执行,其内存从堆栈中清除。
所以,执行顺序是 bar -> foo 但分辨率是后进先出顺序 (LIFO) foo 完成 -> bar 完成。
这就是使它成为“堆栈”的原因。
这里要注意的重要一点是,函数使用的资源只有在执行完毕后才会被释放。当它内部的所有函数和它们内部的函数完成执行时,它就完成了执行。所以你可以有一个非常深的调用堆栈,比如a -> b -> c -> d -> e 并且如果a 中有任何大型资源,你将需要b到e 在它们发布之前完成。
在递归中,一个函数调用自己,它仍然在堆栈上创建条目。所以,如果a 不断调用自己,你最终会得到一个调用堆栈a -> a -> a -> a 等等。
这是一个非常简短的说明
// a very naive recursive count down function
function recursiveCountDown(count) {
//show that we started
console.log("-> start recursiveCountDown [" + count + "]");
if (count !== 0) {//exit condition
//take one off the count and recursively call again
recursiveCountDown(count -1);
console.log("<- end recursiveCountDown [" + count + "]"); // show where we stopped. This will terminate this stack but only after the line above finished executing;
} else {
console.log("<<<- it's the final recursiveCountDown! [" + count + "]"); // show where we stopped
}
}
console.log("--shallow call stack--")
recursiveCountDown(2);
console.log("--deep call stack--")
recursiveCountDown(10);
这是一个非常简单且有缺陷的递归函数,但它仅用于演示在这种情况下会发生什么。
事件队列
JavaScript 在事件队列(或“事件循环”)中运行,简单来说,它等待“活动”(事件),处理它们,然后再次等待。
如果有多个事件,它会按顺序处理它们 - 先进先出 (FIFO),因此是一个队列。所以,如果我们重写上面的函数:
function foo() {
console.log("-> start [foo]");
console.log("<- end [foo]");
}
function bar() {
console.log("-> start [bar]");
console.log("<- end [bar]");
}
function baz() {
console.log("-> start [baz]");
setTimeout(foo, 0);
setTimeout(bar, 0);
console.log("<- end [baz]");
}
baz();
这是如何发挥作用的。
-
baz 被执行。在堆栈上分配的内存。
-
foo 被调度为“下一个”运行而被延迟。
-
bar 被调度为“下一个”运行而被延迟。
-
baz 完成。堆栈已清除。
- 事件循环选择队列中的下一项 - 这是
foo。
-
foo 被执行。在堆栈上分配的内存。
-
foo 完成。堆栈已清除。
- 事件循环选择队列中的下一项 - 这是
bar。
-
bar 被执行。在堆栈上分配的内存。
-
bar 完成。堆栈已清除。
如您所见,堆栈仍在发挥作用。您调用的任何函数都将始终生成一个堆栈条目。事件队列是一个单独的机制。
通过这种方式,您可以获得更少的内存开销,因为您不必等待任何其他函数来释放分配的资源。另一方面,您不能依赖任何功能的完整。
我希望这部分也能回答你的 Q3。
回答 2
推迟到队列有什么帮助?
我希望上面的解释能让它更清楚,但需要确保解释是有道理的:
堆栈的深度是有限制的。如果您考虑一下,这应该很明显 - 可能只有这么多内存可以用于临时存储。一旦达到最大调用深度,JavaScript 将抛出 RangeError: Maximum call stack size exceeded 错误。
如果您查看我上面给出的recursiveCountDown 示例,很容易导致错误 - 如果您调用recursiveCountDown(100000),您将得到RangeError。
通过将所有其他执行放入队列中,您可以避免填满堆栈,从而避免RangeError。所以让我们重新编写函数
// still naive but a bit improved recursive count down function
function betterRecursiveCountDown(count) {
console.log("-> start recursiveCountDown [" + count + "]");
if (count !== 0) {
//setTimeout takes more than two parameters - anything after the second one will be passed to the function when it gets executed
setTimeout(betterRecursiveCountDown, 0, count - 1);
console.log("<- end recursiveCountDown [" + count + "]");
} else {
console.log("<<<- it's the final recursiveCountDown! [" + count + "]"); // show where we stopped
}
}
betterRecursiveCountDown(10);