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

Tec*_*ner 4 javascript recursion

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

var list = readHugeList();

var nextListItem = function() {
    var item = list.pop();

    if (item) {
        // process the list item...
        nextListItem();
    }
};
Run Code Online (Sandbox Code Playgroud)

如果数组列表太大,以下递归代码将导致堆栈溢出.你如何解决这个问题仍然保留递归模式?

提到的解决方案是这样的:

var list = readHugeList();

var nextListItem = function() {
    var item = list.pop();

    if (item) {
        // process the list item...
        setTimeout( nextListItem, 0);
    }
};
Run Code Online (Sandbox Code Playgroud)

解:

由于事件循环处理递归而不是调用堆栈,因此消除了堆栈溢出.当nextListItem运行时,如果item不为null,则将超时函数(nextListItem)推送到事件队列并退出函数,从而使调用堆栈保持清零.当事件队列运行其超时事件时,将处理下一个项目并设置计时器以再次调用nextListItem.因此,在没有直接递归调用的情况下从开始到结束处理该方法,因此无论迭代次数如何,调用栈都保持清晰.

现在我的问题:

Q1)"事件队列"和"调用堆栈"之间有什么区别

Q2)我不明白答案.有人可以详细解释我吗?

Q3)当我在javascript中执行函数或调用变量或对象时.流程怎么样? 什么在调用堆栈?(比方说我做setTimeout ..它是去callstack还是事件队列?)

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

请帮忙!

VLA*_*LAZ 15

答案1和3

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

调用堆栈(简单概述):

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

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

foo();
Run Code Online (Sandbox Code Playgroud)

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

现在,如果你有嵌套函数

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

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

bar();
Run Code Online (Sandbox Code Playgroud)

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

  1. bar 执行 - 为堆栈分配内存.
  2. bar 打印"开始"
  3. foo执行 - 为堆栈分配内存.NB! bar仍在运行,它的内存也存在.
  4. foo 打印"开始"
  5. foo 打印"结束"
  6. foo 完成执行并从堆栈中清除其内存.
  7. bar 打印"结束"
  8. bar 完成执行并从堆栈中清除其内存.

因此,执行顺序是bar- > foo但是分辨率在最后,先出顺序(LIFO)foo结束 - > bar完成.

这就是它成为"堆叠"的原因.

这里需要注意的重要一点是,函数使用的资源只有在完成执行时才会释放.当它内部的所有函数和它们内部的函数完成执行时,它完成执行.所以,你可以像一个很深的调用堆栈a- > b- > c- > d- > e如果有任何大的资源,举办了a您需要be完成他们被释放之前.

在递归中,函数调用自身,它仍然在堆栈上创建条目.所以,如果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);
Run Code Online (Sandbox Code Playgroud)

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

事件队列

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();
Run Code Online (Sandbox Code Playgroud)

这是如何发挥作用.

  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);
Run Code Online (Sandbox Code Playgroud)

  • BRAVO MAN BRAVO !! 这是我在这个问题上看到的最令人惊讶的解释.非常感谢!为你的时间!这是SO如此惊人和令人上瘾的最好理由之一! (3认同)