且构网

分享程序员开发的那些事...
且构网 - 分享程序员编程开发的那些事

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

更新时间:2022-04-12 09:52:30

答案1& 3



事件队列和调用堆栈之间存在很大差异。事实上,它们几乎没有任何共同之处。

Answer 1 & 3

There is a very big difference between event queue and call stack. In fact, they have almost nothing in common.

当你执行一个函数时,它所使用的一切都被称为在堆栈,这是你所指的那个调用堆栈。非常简化,它是功能执行的临​​时内存。或换句话说

When you execute a function everything it uses is said to go on the stack, which is the same call stack you're referring to there. Very simplified, it's temporary memory for functional execution. Or in other words

function foo() {
  console.log("-> start [foo]");
  console.log("<- end   [foo]");
}

foo();

当被调用时,它会被赋予一个小沙箱来与堆栈中的一起玩。当函数结束时,使用的临时内存将被擦除并可用于其他内容。因此,所使用的资源(除非在系统的某个地方给出)只会持续该函数持续的时间。

when called it would be given a little sandbox to play with on the stack. When the function ends, the temporary memory used would be wiped and made available to other things. So, the resources used (unless given somewhere to the system) will only last as long as the function lasts.

现在,如果你有嵌套函数

Now, if you have nested functions

function foo() {
  console.log("-> start [foo]");
  console.log("<- end   [foo]");
}

function bar() {
  console.log("-> start [bar]");
  foo()
  console.log("<- end   [bar]");
}

bar();

以下是您致电该函数时的情况:

Here is what happens when you call the function:


  1. bar 被执行 - 内存在堆栈上分配给它。

  2. bar 打印start

  3. foo 执行 - 为堆栈分配内存。 NB! bar 仍在运行,其内存也在那里。

  4. foo 打印开始

  5. foo 打印结束

  6. foo 完成执行并从堆栈中清除其内存。

  7. bar 打印结束

  8. bar 完成执行并从堆栈中清除其内存。

  1. bar gets executed - memory is allocated on the stack for it.
  2. bar prints "start"
  3. foo gets executed - memory is allocated on the stack for it. NB! bar is still running and its memory is also there.
  4. foo prints "start"
  5. foo prints "end"
  6. foo finishes execution and its memory is cleared from the stack.
  7. bar prints "end"
  8. bar finishes execution and its memory is cleared from the stack.

因此,执行顺序是 bar - > foo 但是分辨率是最后的,先退出订单(LIFO) foo 完成 - > bar 完成。

So, the execution order is bar -> foo but the resolution is in last in, first out order (LIFO) foo finishes -> bar finishes.

这就是它成为堆栈的原因。

And this is what makes it a "stack".

这里要注意的重要一点是使用的资源函数只有在完成执行时才会被释放。当它内部的所有函数和它们内部的函数完成执行时,它完成执行。所以你可以有一个非常深的调用堆栈,如 a - > b - > c - > d - > e 以及是否有任何大型资源以 a 您需要 b e 才能在发布之前完成。

The important thing to note here is that the resources used by a function will only be released when it finishes executing. And it finishes execution when all the functions inside it and those inside them finish executing. So you could have a very deep call stack like a -> b -> c -> d -> e and if any large resources are held in a you would need b to e to finish before they are released.

在递归中,一个函数调用自身,它仍然在堆栈上创建条目。所以,如果 a 一直在调用自己,你最终会得到一个的调用堆栈 - > a - > a - > a 等。

In recursion a function calls itself which still makes entries on the stack. So, if a keeps calling itself you end up with a call stack of a -> a -> a -> a etc.

这是一个非常简短的插图

Here is a very brief illustration

// 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);

这是一个非常简单且非常有缺陷的递归函数,但它只用于演示什么在这种情况下发生。

This is a very simplistic and very flawed recursive function but it only serves to demonstrate what happens in that case.

JavaScript在事件队列中运行(或者也是事件循环 ),简单来说,等待活动(事件),处理它们然后再等待。

JavaScript is ran in an event queue (or also "event loop") which, in simple terms, waits for "activity" (events), processes them and then waits again.

如果有多个事件,它将按顺序处理它们 - 先进先出(FIFO),因此成为一个队列。所以,如果我们重新编写上述函数:

If there are multiple events, it would process them in order - first in, first out (FIFO), hence a queue. So, if we re-write the above functions:

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

这是如何发挥作用的。

Here is how this plays out.


  1. baz 已执行。在堆栈上分配的内存。

  2. foo 因计划运行下一步而延迟。

  3. bar 因计划运行下一步而延迟。

  4. baz 完成。堆栈已清除。

  5. 事件循环选择队列中的下一项 - 这是 foo

  6. foo 被执行。在堆栈上分配的内存。

  7. foo 完成。堆栈被清除。

  8. 事件循环选择队列中的下一个项目 - 这是 bar

  9. bar 被执行。在堆栈上分配的内存。

  10. bar 完成。堆栈已清除。

  1. baz is executed. Memory allocated on the stack.
  2. foo is delayed by scheduling it to run "next".
  3. bar is delayed by scheduling it to run "next".
  4. baz finishes. Stack is cleared.
  5. Event loop picks the next item on the queue - this is foo.
  6. foo gets executed. Memory allocated on the stack.
  7. foo finishes. Stack is cleared.
  8. Event loop picks the next item on the queue - this is bar.
  9. bar gets executed. Memory allocated on the stack.
  10. bar finishes. Stack is cleared.

正如您所希望的那样,堆栈仍在使用中。您调用的任何函数将始终生成堆栈条目。事件队列是一个单独的机制。

As you can hopefully see, the stack is still in play. Any function you call will always generate a stack entry. The event queue is a separate mechanism.

使用这种方式,你可以获得更少的内存开销,因为你不需要在任何其他函数上释放分配资源。另一方面,你不能依赖任何完成的功能。

With this way of doing things, you get less memory overhead, since you do not have to way on any other function to release the allocated resources. On the other hand, you cannot rely on any function being complete.

我希望这部分也能回答你的Q3。

I hope this section also answers your Q3.

如何推迟到队列帮助?

我希望上面的解释能够做到它更清楚,但它确保解释有意义:

I hope the above explanation will make it more clear but it bears making sure the explanation makes sense:

堆栈的深度有一个设定的限制。如果你考虑一下,它应该是显而易见的 - 只有大量的内存可以用来推测临时存储。一旦达到最大调用深度,JavaScript将抛出 RangeError:超出最大调用堆栈大小错误。

There is a set limit of how deep the stack could be. If you think about it, it should be obvious - there is only so much memory to spare for presumably temporary storage. Once the maximum call depth is reached, JavaScript will throw RangeError: Maximum call stack size exceeded error.

如果你看看上面给出的 recursiveCountDown 示例,很容易导致错误 - 如果你调用 recursiveCountDown(100000)你将得到 RangeError

If you look at the recursiveCountDown example I gave above that can very easily be made to cause an error - if you call recursiveCountDown(100000) you will get the RangeError.

通过将所有其他执行放在队列中,你可以避免填写堆栈,因此你避免 RangeError 。所以让我们重新编写函数

By putting every other execution on the queue, you avoid filling up the stack and thus you avoid the RangeError. So let's re-write the function

// 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);