使用setTimeout避免堆栈溢出

Avoiding stack overflow by using setTimeout

本文关键字:堆栈 栈溢出 setTimeout 使用      更新时间:2023-09-26

我在这里发现了以下问题:

如果数组列表太大。如何修复此问题并仍然保留递归图案

答案是:

可以通过修改nextListItem函数如下:

var list = readHugeList();
var nextListItem = function() {
    var item = list.pop();
    if (item) {
        // process the list item...
        setTimeout( nextListItem, 0);
    }
};

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

有人能给我解释一下吗:

  1. 这个用例是否实用
  2. 为什么长数组会导致堆栈溢出

这只是蹦床的一种巧妙替代品,而蹦床又只是TCO的一种拙劣替代品。

当您在Javascript中调用函数时,您会在调用堆栈中添加一个帧。该框架包含有关函数作用域中的变量以及如何调用函数的信息。

在调用函数之前,调用堆栈是空的。

-------

如果我们调用函数foo,那么我们将在堆栈的顶部添加一个新帧。

| foo |
-------

foo完成执行时,我们再次将帧从堆栈中弹出,使其再次为空。

现在,如果foo依次调用另一个函数bar,那么我们需要在执行foo的同时向堆栈中添加一个新帧。

| bar |
| foo |
-------

希望您能看到,如果一个函数递归地调用自己,它会不断向调用堆栈的顶部添加新的帧。

| ...          |
| nextListItem |
| nextListItem |
| nextListItem |
| nextListItem |
----------------

递归函数将不断添加帧,直到它们完成处理,或者超过调用堆栈的最大长度,从而导致溢出。

因为setTimeout是一个异步操作,所以它不会阻塞您的函数,这意味着nextListItem将被允许完成,并且它的帧可以从调用堆栈中弹出,从而防止它增长。递归调用将改为使用事件循环处理。

这种模式有用吗调用堆栈的最大大小取决于您的浏览器,但可能低至1130。如果您想使用递归函数处理一个包含几千个元素的数组,那么您就有破坏调用堆栈的风险。

蹦床使用类似的技术,但不是将工作转移到事件循环,而是返回一个调用下一次迭代的函数,然后可以使用while循环来管理调用(这不会影响堆栈)。

var nextListItem = function() {
  var item = list.pop();
  if (item) {
    // process the list item...
    return nextListItem;
  }
};
while(recur = recur()) {}
  1. 通常情况下不会,但如果您决定需要为长序列递归地链接相同的函数调用,这可能会派上用场。

  2. 当为特定程序分配的堆栈内存量已被充分利用时,递归操作期间会发生堆栈溢出。递归遍历足够长的数组可能会导致堆栈溢出。也许您不了解调用堆栈是如何工作的?

对于发布的代码,重复的for循环是最有效的。但是,如果您的实际代码不能适合使用for循环,那么还有另一种选择。

setTimeout的使用取决于你对"实用性"的看法,所以让我们列出事实,这样你就可以自己决定了。

  • setTimeout迫使浏览器用获得毫秒精度计时的操作淹没CPU。这可能是非常低效的电力
  • 使用setTimeout,每次迭代将花费4ms。仅4次迭代就需要整个帧的渲染时间。250次迭代,整整一秒钟过去了

setTimeout还有一个替代方案,可以帮助您在不使用setTimeout的情况下实现您想要做的事情:DeferStackJS库。如果你使用DeferStackJS,那么你需要做的就是如下。

var list = readHugeList();
var nextListItem = function() {
    var item = list.pop();
    if (item) {
        // process the list item...
        DeferStack( nextListItem );
    }
};

请强调,上面的代码片段旨在演示如何集成DeferStackJS说实话,使用DeferStackJS或Array.prototype.pop都不适合这个特定的代码片段。相反,下面的代码会轻而易举地击败他们。

var list = readHugeList();
var nextListItem = function() {
    "use strict";
    var item, i = list.length;
    while (i--) { // Bounds check: as soon as i === -1, the while loop will stop.
                  // This is because i-- decrements i, returning the previous
                  // value of i
        item = list[i];
        if (!item) break; // break as soon as it finds a falsey item 
        // Place code here to be executed if it found a truthy value:
        // process the list item...
    }
    if (i !== -1) {
        // Place code here to be executed if it found a falsey value:
    }
};

提到DeferStackJS的唯一原因是,我坚信回答论坛的人的首要职责是回答最初提出的问题。然后,在他们回答后,他们可以发表评论,对这样问的问题进行猜测。