在JavaScript中asynchronous迭代大量数组,而不会触发超出的堆栈大小

我的环境是NodeJS,尽pipe这也可能是Web相关的问题。 我有一个数据库的大量数据,我试图枚举。 然而,为了争论让我说,我有一个20000string的数组:

var y = 'strstrstrstrstrstrstrstrstrstr'; var x = []; for(var i = 0; i < 20000; i++) x.push(y); 

我想枚举这个列表asynchronous,可以说使用asynchronous库 ,并可以说,因为我超级谨慎,我甚至一次限制我的枚举5次迭代:

 var allDone = function() { console.log('done!') }; require('async').eachLimit(x, 5, function(item, cb){ ... someAsyncCall(.., cb); }, allDone); 

期望的是5个x的项目将在上面同时迭代,并且最终所有20,000个项目将被迭代,并且控制台将打印“完成!”。 实际发生的是:

 Uncaught exception: [RangeError: Maximum call stack size exceeded] 

在这一点上,我认为这一定是asynchronous库的一种错误,所以我写了自己的eachLimit版本,如下所示:

 function eachLimit(data, limit, iterator, cb) { var consumed = 0; var consume; var finished = false; consume = function() { if(!finished && consumed >= data.length) { finished = true; cb(); }else if(!finished) { return iterator(data[consumed++], consume); } }; var concurrent = limit > data.length ? data.length : limit; for(var i = 0; i < concurrent; i++) consume(); } 

有趣的是,这解决了我的问题。 但是当我将我的实验从nodeJS移到Chrome时,即使使用上面的解决scheme,我仍然会收到堆栈大小。

显然,我的方法不会像async中包含的eachLimit方法一样增加堆栈。 不过,我仍然认为我的方法是不好的,因为可能不是20k项目,但对于一些大小的数组,我仍然可以使用我的方法超过堆栈大小。 我觉得我需要使用尾recursion来devise这个问题的某种解决scheme,但我不确定v8是否会针对这种情况进行优化,或者是否有可能解决这个问题。

我觉得我需要使用尾recursion来devise这个问题的某种解决scheme,但我不确定v8是否会针对这种情况进行优化,或者是否有可能解决这个问题。

你正在使用的continuation-passing-style已经是尾recursion(或者接近)。 问题是,大多数JS引擎真的倾向于在这种情况下做stackoverflows。

有两个主要的方法来解决这个问题:

1)使用setTimeout强制代码是asynchronous的。

你的代码发生了什么事情,你是在原函数返回之前调用callback函数。 在一些asynchronous库中,这将最终导致stackoverflow。 一个简单的解决方法是强制callback只能在事件处理循环的下一次迭代中运行,方法是将其包装在setTimeout中。 翻译

 //Turns out this was actually "someSyncCall"... someAsyncCall(.., cb); 

 someAsyncCall(..., function(){ setTimeout(cb, 0) }); 

这里的主要优点是这非常简单。 缺点是这会给你的循环增加一些延迟,因为setTimeout被实现,所以callback总是会有一些非零的延迟(即使你把它设置为零)。 在服务器上,你可以使用nextTick(或者像这样的事情,忘记了确切的名字)来做类似的事情。

也就是说,它有一个奇怪的是有一个大循环顺序asynchronous操作。 如果您的操作实际上是asynchronous的,那么由于networking延迟,要花费数年才能完成。

2)使用蹦床来处理同步代码。

100%避免一个堆栈溢出的唯一方法是使用真正的while循环。 有了承诺,这将是更容易编写的伪代码:

 //vastly incomplete pseudocode function loopStartingFrom(array, i){ for(;i<array.length; i++){ var x = run_next_item(i); if(is_promise(x)){ return x.then(function(){ loopStartingFrom(array, i+1) }); } } } 

基本上,你在一个实际的循环中运行你的循环,用一些方法来检测你的一个迭代是否立即返回或者推迟到一个asynchronous计算。 当事情立即返回时,你保持循环运行,当你最终得到一个真正的asynchronous结果时,你停止循环,并在asynchronous迭代结果完成时恢复它。

使用蹦床的缺点是它稍微复杂一些。 也就是说,有一些asynchronous库,保证不会发生stackoverflow(通过使用我提到的两个技巧之一)。

为了防止堆栈溢出,您需要避免consume自身的recursion。 你可以用一个简单的标志来做到这一点:

 function eachLimit(data, limit, iterator, cb) { var consumed = 0, running = 0, async = true; function consume() { running--; if (!async) return; while (running < limit && consumed < data.length) { async = false; running++; iterator(data[consumed++], consume); async = true; } if (running == 0) cb(); } running++; consume(); } 

你有没有考虑使用承诺呢? 他们应该解决一个不断增加的问题(也可以使用承诺,这在我的书中是一个很大的优势):

 // Here, iterator() should take a single data value as input and return // a promise for the asynchronous behavior (if it is asynchronous) // or any value if it is synchronous function eachLimit(data, limit, iterator) { return Promise(function (resolve, reject) { var i = 0; var failed = false; function handleFailure(error) { failed = true; reject(error); } function queueAction() { try { Promise.when(iterator(data[i])) .then(handleSuccess, handleFailure); } catch (error) { reject(error); } } function handleSuccess() { if (!failed) { if (i < data.length) { queueAction(); i += 1; } else { resolve(); } } } for (; i < data.length && i < limit; i += 1) { queueAction(); } }); }