这段基于 Promise 的递归为什么不会爆栈?求 JS 大拿

2018-09-07 09:34:01 +08:00
 pofeng

写了个轮询检查的工具函数,考虑到递归爆栈的可能,测试了一下。

预料中会爆栈,结果居然能正常运行,惊了

代码如下:

function pollingCheck(fn, delay) {
  return fn().catch(() => delay().then(() => pollingCheck(fn, delay)))
}

let count = 0
pollingCheck(
  () => count++ > 200000 ? Promise.resolve() : Promise.reject(),
  () => Promise.resolve() //TODO
).then(() => console.log('finish'))

根据 google 到的最大栈数,最多也就 5 万,下面这段递归轻轻松松就爆栈了

function main(n) {
  if (n === 0) {
    return 'finish'
  } else {
    return main(n - 1)
  }
}

console.log(main(200000))

所以,为什么上面的 Promsie 递归不会爆栈?

6562 次点击
所在节点    JavaScript
20 条回复
kingcc
2018-09-07 09:51:22 +08:00
我猜因为你 return 的是 Promise instance,下面的是真正的递归
kingcc
2018-09-07 09:55:48 +08:00
简单来说就是上面的函数抛出了一个 pending promise,不算是递归因为它执行完了。
kingwl
2018-09-07 09:56:50 +08:00
没有递归 怎么会爆
wxsm
2018-09-07 10:19:47 +08:00
实际上你这个是线性执行的吧。如楼上所说,没有递归。
pofeng
2018-09-07 10:22:43 +08:00
@kingcc 但是返回的 promise 带了 pollingCheck 内声明的箭头函数,其闭包有引用 fn 和 delay,不会导致 pollingCheck 的运行栈无法释放么?
zyEros
2018-09-07 10:24:16 +08:00
其实很简单,你看一些主流的 Promise 为了实现 Promise 的 lazy 特性的时候,他们都会用到类似于 setTimeout/setImmediate 之类的函数,当然 NativePromise 是直接用的 mirco,例如 q ( https://github.com/kriskowal/q/blob/master/q.js#L150 ),所以当你创建一个 Promise 的时候,他的执行其实是依赖于 setTimeout 的

setTimeout 实际上不会爆,因为 setTimeout 之类的函数依赖的是事件循环,你在 setTimeout 之类注册的函数在 JS Engine 层面可以看做一个对象,setTimeout 的无非只是把这个对象放到了事件循环队列里面等待触发,所以他根本不是递归执行的嘛(逃
kingcc
2018-09-07 10:30:02 +08:00
我说了嘛,简单来说…

运行栈等到你执行一次 delay 就释放了一个,你要是还不明白我就画一个图…
zyEros
2018-09-07 10:40:11 +08:00
提供一个例子:
```javascript
function x() {
new Promise(resolve => {
resolve();
x();
});
}
x();
```
DOLLOR
2018-09-07 10:57:00 +08:00
递归不一定会爆栈的,比如尾递归
AnonymousUser
2018-09-07 11:06:27 +08:00
@DOLLOR js 没有尾递归优化,一样爆栈
zyEros
2018-09-07 11:07:30 +08:00
例子提供错了:
function x() {
Promise.resolve().then(x);
}
x();

这个其实和你:
function x() {
setTimeout(x,0);
}
x();

效果是一样的
SakuraKuma
2018-09-07 11:40:20 +08:00
上面都说了,microtask/macrotask 不会卡着主线程
你第二个例子是主线程的。
而且如#9 所说,尾递归的栈帧处理也不会爆掉。

(本人拙见
pofeng
2018-09-07 11:44:31 +08:00
@kingcc @zyEros 大概弄明白了,因为 Promise 会再另外一个 Task 运行的的原因所以不会爆栈,而 pollingCheck 的 scope 会释放,不会形成一个很长的链
leemove
2018-09-07 12:00:45 +08:00
哇,这些天天争 Vue,React,Angular 的大手,都被一个 bridgePromise 卡住了...还有 Promise 是走异步事件循环的.
maplerecall
2018-09-07 12:15:03 +08:00
@AnonymousUser es6 已经有尾递归优化了,js 发展还是很快的
orangemi
2018-09-07 12:40:03 +08:00
问题是为什么不会爆栈,实际上 Promise.then 是把所有的栈都丢掉了,所以不爆栈。
题主可以尝试使用 new Error().stack 查看左后一次的栈,之前几万次的 stack 都没有了。
nodejs 一个 tick 间只有一个栈,调用 Promise.then 中间的过程中,已经走到了另外一个 tick。
leemove
2018-09-07 12:46:09 +08:00
@maplerecall js 的尾递归在 V8 上默认是不开启的,在 Node 中也需要对 v8 特殊配置才可以.
otakustay
2018-09-07 12:53:27 +08:00
异步会清栈,所以 Promise 递归爆不掉
箭头函数产生的是作用域,不是栈,这个要分清
Sparetire
2018-09-07 13:54:59 +08:00
其实就是函数调用栈转成了异步任务队列了。。同一时刻的内存是有限的,然而即便是无限地递归,转成任务队列这些内存占用也分散在了无限的时间中。。
xieranmaya
2018-09-09 18:54:25 +08:00
异步递归不是递归,实际上连调用栈都没有,或者说调用栈里就那一个函数

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/486982

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX