我对理解“事件队列”和“调用堆栈”概念的好奇心在我解决这个问题时开始:
var list = readHugeList();
var nextListItem = function() {
var item = list.pop();
if (item) {
// process the list item...
nextListItem();
}
};
如果数组列表太大,以下递归代码将导致堆栈溢出。你如何解决这个问题并仍保留递归模式?
提到的解决方案是:
var list = readHugeList();
var nextListItem = function() {
var item = list.pop();
if (item) {
// process the list item...
setTimeout( nextListItem, 0);
}
};
解决方案:
由于事件循环处理了堆栈溢出,因此消除了堆栈溢出 递归,而不是调用堆栈。当nextListItem运行时,如果item不是 null,超时函数(nextListItem)被推送到事件队列 并且函数退出,从而使调用堆栈清晰。当。。。的时候 事件队列运行其超时事件,处理下一个项目并且a timer设置为再次调用nextListItem。因此,该方法是 从头到尾处理没有直接递归调用,所以 无论迭代次数多少,调用堆栈都会保持清晰。
现在我的问题:
Q1)“事件队列”和“调用堆栈”之间有什么区别
Q2)我不明白答案。有人可以详细解释我吗?
Q3)当我在javascript中执行函数或调用变量或对象时。流程如何? 调用堆栈中的内容是什么?(假设我执行setTimeout ..它是去callstack还是事件队列?)
这些概念非常不清楚。我用谷歌搜索,但大部分结果都不是我期望理解的。
请帮忙!
答案 0 :(得分:14)
事件队列和调用堆栈之间存在很大差异。事实上,它们几乎没有任何共同之处。
当你执行一个函数时,它所使用的所有内容都被认为是在堆栈上的 ,这与你在那里引用的调用堆栈相同。非常简化,它是功能执行的临时内存。或者换句话说
function foo() {
console.log("-> start [foo]");
console.log("<- end [foo]");
}
foo();
&#13;
当被调用时,它将被赋予一个小沙箱来与堆栈中的一起玩。当函数结束时,使用的临时内存将被擦除并可用于其他内容。因此,所使用的资源(除非在系统的某个地方给出)将只持续该函数持续的时间。
现在,如果您有嵌套函数
function foo() {
console.log("-> start [foo]");
console.log("<- end [foo]");
}
function bar() {
console.log("-> start [bar]");
foo()
console.log("<- end [bar]");
}
bar();
&#13;
以下是调用该函数时发生的情况:
bar
- 为堆栈分配内存。bar
打印&#34;开始&#34; foo
- 为堆栈分配内存。 NB! bar
仍在运行,其内存也在那里。foo
打印&#34;开始&#34; foo
打印&#34;结束&#34; foo
完成执行并从堆栈中清除其内存。bar
打印&#34;结束&#34; bar
完成执行并从堆栈中清除其内存。因此,执行顺序为bar
- &gt; foo
,但决议是最后一次,先退出(LIFO)foo
完成 - &gt; bar
完成。
这就是使它成为&#34;堆栈的原因。
这里需要注意的重要一点是,函数使用的资源只有在完成执行时才会释放。当它内部的所有函数和它们内部的函数完成执行时,它完成执行。所以你可以有一个非常深的调用堆栈,如a
- &gt; b
- &gt; c
- &gt; d
- &gt; e
如果a
中有任何大型资源,您需要b
到e
才能完成发布。
在递归中,函数调用自身,它仍然在堆栈上创建条目。因此,如果a
一直在调用自己,那么最终会调用a
- &gt;的调用堆栈。 a
- &gt; a
- &gt; 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);
&#13;
这是一个非常简单且非常有缺陷的递归函数,但它仅用于演示在这种情况下会发生什么。
JavaScript在事件队列中运行(或者也是#34;事件循环&#34;),简单来说,等待&#34;活动&#34; (事件),处理它们然后再等待。
如果有多个事件,它将按顺序处理它们 - 先进先出(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();
&#13;
这是如何发挥作用的。
baz
已执行。内存分配在堆栈上。foo
通过安排运行&#34; next&#34;。bar
通过安排运行&#34; next&#34;。baz
完成。堆栈已清除。foo
。foo
被执行。内存分配在堆栈上。foo
完成。堆栈已清除。bar
。bar
被执行。内存分配在堆栈上。bar
完成。堆栈已清除。正如您所希望的那样,堆栈仍在使用中。您调用的任何函数将始终生成堆栈条目。事件队列是一个单独的机制。
通过这种方式,您可以减少内存开销,因为您不必等待任何其他功能来释放分配的资源。另一方面,你不能依赖任何完成的功能。
我希望本节也能回答你的问题。
如何推迟到队列帮助?
我希望上面的解释会更清楚,但它确保解释是有道理的:
堆栈的深度有一个限制。如果你考虑一下,它应该是显而易见的 - 只有大量的内存可用于大概是临时存储。达到最大调用深度后,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);
&#13;
答案 1 :(得分:1)
使用call stack
的主要原因用于了解当前函数结束后的去向。
但大多数语言的大小限制为call stack
,因此如果重复调用函数直到函数未完成,call stack
的大小就会溢出。
setTimeout
的大多数工具都有queue
用于保存工作。
并在空闲时间执行它们。
首先nextListItem
在自身未完成之前调用自我。
所以call stack
会很长,直到项目列表结束。
第二个nextListItem
在完成后自称为自我,call stack
也是明确的。
因此,call stack
在空闲时从nextListItem
调用setTimeout
时将从空开始。
call stack
用于函数调用历史记录。 event queue
用于保存setTimeout
作业。
见上解释。
javascript只是不断执行你的陈述。但是在函数完成后调用此函数返回的位置将保存。 call stack
用于保存被调用函数的历史记录。