了解 JavaScript 中的事件队列和调用堆栈

Understanding Event Queue and Call stack in javascript

本文关键字:调用 堆栈 事件队列 JavaScript 了解      更新时间:2023-09-26

我对理解"事件队列"和"调用堆栈"概念的好奇心始于我解决这个问题:

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中执行函数或调用变量或对象时。流程如何?调用堆栈中有什么?(假设我设置超时。它是转到调用堆栈还是事件队列?

这些概念非常不清楚。我用谷歌搜索了一下,但大多数结果都不是我期望理解的。

请帮忙!

答案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();

以下是调用函数时发生的情况:

  1. bar被执行 - 在堆栈上为其分配内存。
  2. bar打印"开始">
  3. foo执行 - 在堆栈上为其分配内存。铌! bar仍在运行,其内存也在那里。
  4. foo打印"开始">
  5. foo打印"结束">
  6. foo完成执行,其内存将从堆栈中清除。
  7. bar打印"结束">
  8. 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();

这是如何发挥作用的。

  1. baz被执行。堆栈上分配的内存。
  2. foo通过安排它运行"下一个"来延迟。
  3. bar通过安排它运行"下一个"来延迟。
  4. baz完成。堆栈已清除。
  5. 事件循环选择队列上的下一项 - 这是foo
  6. foo被执行。堆栈上分配的内存。
  7. foo结束。堆栈已清除。
  8. 事件循环选择队列上的下一项 - 这是bar
  9. bar被执行。堆栈上分配的内存。
  10. 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将从空开始。

  1. call stack是为函数调用历史记录而制作的,event queue是为保存setTimeout作业而制作的。

  2. 见上解释。

  3. JavaScript 只是不断地执行你的语句。 但将保存在调用此函数的位置,以便在函数完成后返回到该函数。 call stack用于保存被调用函数的历史记录。