答案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
打印“start”。
- 执行
foo
- 在堆栈上为其分配内存。注意! bar
仍在运行,它的内存也在那里。
foo
打印“start”。
foo
打印“end”。
foo
完成执行,并从堆栈中清除其内存。
bar
打印“end”。
bar
完成执行,并从堆栈中清除其内存。
因此,执行顺序是bar
-> foo
,但解析顺序是后进先出(LIFO)foo
完成 -> bar
完成。
这就是它成为“堆栈”的原因。
这里需要注意的重要事项是,只有当函数执行完成时,它所使用的资源才会被释放。并且它执行结束的时间是在其内部所有函数和这些函数内部的所有函数都执行完毕之后。因此,你可能会有一个非常深的调用堆栈,例如a
-> b
-> c
-> d
-> e
,如果a
中保存有大量资源,则需要等待b
到e
都执行完毕后这些资源才能被释放。
在递归中,函数会调用自身,这仍然会在堆栈上创建新的条目。因此,如果a
不断地调用自身,那么就会得到一个调用堆栈,例如a
-> a
-> a
-> a
等等。
以下是一个非常简要的示例:
function recursiveCountDown(count) {
console.log("-> start recursiveCountDown [" + count + "]");
if (count !== 0) {
recursiveCountDown(count -1);
console.log("<- end recursiveCountDown [" + count + "]");
} else {
console.log("<<<- it's the final recursiveCountDown! [" + count + "]");
}
}
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();
这是其运作方式。
1. 执行 `baz`,在堆栈上分配内存。
2. 通过将其调度为“下一个”运行,延迟执行 `foo`。
3. 通过将其调度为“下一个”运行,延迟执行 `bar`。
4. `baz` 完成。堆栈被清除。
5. 事件循环选择队列中的下一项——这是 `foo`。
6. 执行 `foo`,在堆栈上分配内存。
7. `foo` 完成。堆栈被清除。
8. 事件循环选择队列中的下一项——这是 `bar`。
9. 执行 `bar`,在堆栈上分配内存。
10. `bar` 完成。堆栈被清除。
正如您所看到的,堆栈仍然起着作用。您调用的任何函数都将始终生成堆栈条目。事件队列是单独的机制。
采用这种方式,您将获得更少的内存开销,因为您不必等待其他任何函数释放已分配的资源。另一方面,您不能依赖任何函数完成。
希望本节也回答了您的第三个问题。
function betterRecursiveCountDown(count) {
console.log("-> start recursiveCountDown [" + count + "]");
if (count !== 0) {
setTimeout(betterRecursiveCountDown, 0, count - 1);
console.log("<- end recursiveCountDown [" + count + "]");
} else {
console.log("<<<- it's the final recursiveCountDown! [" + count + "]");
}
}
betterRecursiveCountDown(10);
setTimeout(nextListItem, 0)
时,setTimeout
会进入调用栈,它会将一个带有nextListItem
的定时器添加到事件队列中,然后返回,也就是从调用栈中弹出setTimeout
。 - BergisetTimeout(nextListItem)
就足够了,不需要指定持续时间。 - vsync