使用事件队列
在这种情况下,您可以轻松地将递归卸载到事件队列中。
var list = readHugeList();
var nextListItem = function() {
var item = list.pop();
if (item) {
console.log("processing", item);
// schedule the processing of the next list item
setTimeout(nextListItem);
}
};
nextListItem();
function readHugeList() {
return [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; //example
}
这是您可以做的最简单的事情 - setTimeout 将安排 nextListItem 再次调用,但首先会清除当前堆栈帧。这样,您就不会导致堆栈溢出,并且可以处理任意大的输入。
注意:这不会通过递归导致堆栈溢出,但是,它会缓慢。问题是the minimum timeout length is 4ms,所以如果每次调用不到4ms就可以完成,那么整个操作会花费更长的时间。例如,如果每个操作需要 4ms,那么通过setTimeout 延迟将需要 4 倍的时间。即使删除了最小超时(例如in Node.js 或using setImmediate in Node.js),它也会比其他替代方案更快但仍然慢 - 这仍然会等待整个事件循环完成。虽然比强制的最少 4 毫秒等待要快,但它仍然是一个额外的开销。
但是,此方法确实允许同时发生其他事情,因此即使它较慢,它也可能是防止阻塞的合适实现。这样做的一种用途是创建增量更新。否则它可能会允许其他不相关的任务发生,因此它们不会被延迟。所以,它是一种有用的技术,但可能不是最好的——如果你有足够大的数据集导致堆栈溢出,那么你肯定会遇到时间问题。
使用蹦床
另一种方法是使用a trampoline。这基本上是tail call optimisation 的手动实现。这是一个示例实现:
function trampoline(fn) {
while (typeof fn === "function") {
fn = fn();
}
return fn;
};
这是一个简单的基础实现——它不考虑参数,但更好的实现会考虑。蹦床有库实现,不用自己写。
无论如何,蹦床还需要更改递归函数 - 而不是返回 result,它必须返回需要评估的 thunk。所以,这是它的工作原理:
var list = readHugeList();
var nextListItem = function() {
var item = list.pop();
if (item) {
console.log("processing", item);
// a thunk for the next list item
return () => nextListItem();
}
};
trampoline(nextListItem);
function trampoline(fn) {
while (typeof fn === "function") {
fn = fn();
}
return fn;
};
function readHugeList() {
return [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; //example
}
(注意:return () => nextListItem() 也可以像 return nextListItem 一样简单,但我想在这里明确)
蹦床将继续调用该函数,直到它停止返回更多要评估的 thunk。并且每个 thunk 本身都将包含对该函数的递归调用。因此,在通过 fn() 评估 thunk 时,您现在可以 间接 调用函数,而不是 直接 调用它自己。而且由于这是在循环中完成的,它仍然可以防止堆栈溢出具有非常深的嵌套递归。
第三个选项使用事件队列并作弊
好吧,这并不是真正的作弊,但你可以给自己一个优势。如前所述,将递归委托给事件队列将解决堆栈溢出问题,但会引入速度问题,因为它对每个延迟操作施加了最小延迟。只是为了演示这将如何影响性能,下面是一个示例:
var list = readHugeList();
var nextListItem = function() {
var item = list.pop();
if (item) {
console.log("processing", item);
// schedule the processing of the next list item
setTimeout(nextListItem);
} else {
console.log("operation took (in ms):", performance.now() - startTime)
}
};
var startTime = performance.now();
nextListItem();
function readHugeList() {
//generate an array of 100 sequential numbers
return Array.from({length: 100}, (_, i) => i);
}
递归地浏览一百个项目的列表大约需要一秒钟(至少在我的机器上)。无论花费多少时间,它都太长了——一百个项目并不多,甚至不会首先导致调用堆栈溢出。我不会尝试使用可能导致溢出的数字,因为它会工作,但等待它完成会非常长。
但是,您仍然可以通过 microtasks 来利用事件循环。一个非常快速的总结是事件循环处理两种类型的任务
- 宏任务(例如
setTimeout 设置的内容)- 它们具有 4 毫秒的最小延迟,并且还允许其他事情发生,例如浏览器动画。
- 微任务 - 它们具有更高的优先级,没有任何最小延迟,并且会尽快执行。
如果您卸载到 micro 任务队列而不是宏任务队列,那么您将获得更快的处理速度。为此,您可以在浏览器中使用 Promise - 已解决的 Promise 会将所需的任何进一步处理(使用 .then 添加的回调)添加到微任务队列。
这是它的外观和行为方式:
var list = readHugeList();
var nextListItem = function() {
var item = list.pop();
if (item) {
console.log("processing", item);
// schedule the processing of the next list item
Promise.resolve()//resolve immediately
.then(nextListItem); //adds a microtask
} else {
console.log("operation took (in ms):", performance.now() - startTime)
}
};
var startTime = performance.now();
nextListItem();
function readHugeList() {
//generate an array of 10 000 sequential numbers
return Array.from({length: 10000}, (_, i) => i);
}
这大约需要二分半(在我的机器上)。这与setTimeout 相当,只是微任务版本处理两个数量级 更多的项目。那是 10 000,而只有 100。