了解 JavaScript 中的事件队列和调用堆栈
Understanding Event Queue and Call stack in javascript
我对理解"事件队列"和"调用堆栈"概念的好奇心始于我解决这个问题:
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);
}
};
溶液:
消除了堆栈溢出,因为事件循环处理 递归,而不是调用堆栈。当下一个列表项运行时,如果项不是 null,超时函数 (nextListItem( 被推送到事件队列 函数退出,从而使调用堆栈保持清除状态。当 事件队列运行其超时事件,处理下一项,然后 计时器设置为再次调用下一个列表项。因此,该方法为 从头到尾处理,没有直接递归调用,所以 无论迭代次数如何,调用堆栈都保持清晰。
现在我的问题:
问题 1("事件队列"和"调用堆栈"有什么区别
问题 2(我不明白答案。有人可以详细解释我吗?
Q3(当我在javascript中执行函数或调用变量或对象时。流程如何?调用堆栈中有什么?(假设我设置超时。它是转到调用堆栈还是事件队列?
这些概念非常不清楚。我用谷歌搜索了一下,但大多数结果都不是我期望理解的。
请帮忙!
和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);
使用call stack
的主要原因是知道当前函数结束后该去哪里。但是大多数语言都有大小限制call stack
因此,如果在函数未完成之前重复调用函数,则call stack
的大小会溢出。
setTimeout
的大多数实现都有保存作业queue
。并在空闲时间起诉他们。
首先nextListItem
在自身尚未完成之前调用自我。因此,call stack
直到项目列表结束的时间会很长。
第二nextListItem
是完成自己后称为自我,call stack
也很清楚。因此,当空闲时间从setTimeout
调用nextListItem
时,call stack
将从空开始。
-
call stack
是为函数调用历史记录而制作的,event queue
是为保存setTimeout
作业而制作的。 -
见上解释。
-
JavaScript 只是不断地执行你的语句。 但将保存在调用此函数的位置,以便在函数完成后返回到该函数。
call stack
用于保存被调用函数的历史记录。
- 是什么让一个“;Uncaught RangeError:超过了最大调用堆栈大小“;错误(Chrome,在其他浏览器中显示
- 超过了async.detect最大调用堆栈大小
- 如何远程检查JavaScript应用程序的函数调用堆栈
- 超过了最大调用堆栈大小,循环无限
- 超过了最大调用堆栈大小.递归标签
- 日志:未捕获的范围错误:超过了最大调用堆栈大小
- 未捕获的范围错误:setTimeout()超过了最大调用堆栈大小
- JavaFX+WebView/Javascript:setTimeOut不起作用调用堆栈来自Java
- JavaScript继承:未捕获的范围错误:超过了最大调用堆栈大小
- 使用$cookies和$stateChangeStart检查sessionID是否超过了最大调用堆栈
- jQuery捕获"RangeError:超过了最大调用堆栈大小“;
- 设置这个.RangeError:超过了最大调用堆栈大小
- 收到“范围错误: 超出最大调用堆栈大小”错误
- 递归 - 测试最大堆栈大小时,调用堆栈无法弹出
- 轮询 ajax 函数超出调用堆栈
- Chrome RangeError:使用jQuery$.map时超过了最大调用堆栈大小
- 要求JS 2.1.9引起“;最大调用堆栈"使用Grunt时出错
- Chrome/jQuery未捕获范围错误:超过了最大调用堆栈大小(函数循环)
- 超过了最大调用堆栈大小-没有明显的递归
- 警告:字体加载过程中出错:轨道上的PDFJS超过了最大调用堆栈大小错误