V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
推荐关注
Meteor
JSLint - a JavaScript code quality tool
jsFiddle
D3.js
WebStorm
推荐书目
JavaScript 权威指南第 5 版
Closure: The Definitive Guide
pofeng
V2EX  ›  JavaScript

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

  •  1
     
  •   pofeng · 2018-09-07 09:34:01 +08:00 · 6562 次点击
    这是一个创建于 2299 天前的主题,其中的信息可能已经有所发展或是发生改变。

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

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

    代码如下:

    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 递归不会爆栈?

    20 条回复    2018-09-09 18:54:25 +08:00
    kingcc
        1
    kingcc  
       2018-09-07 09:51:22 +08:00 via Android
    我猜因为你 return 的是 Promise instance,下面的是真正的递归
    kingcc
        2
    kingcc  
       2018-09-07 09:55:48 +08:00 via Android
    简单来说就是上面的函数抛出了一个 pending promise,不算是递归因为它执行完了。
    kingwl
        3
    kingwl  
       2018-09-07 09:56:50 +08:00
    没有递归 怎么会爆
    wxsm
        4
    wxsm  
       2018-09-07 10:19:47 +08:00
    实际上你这个是线性执行的吧。如楼上所说,没有递归。
    pofeng
        5
    pofeng  
    OP
       2018-09-07 10:22:43 +08:00
    @kingcc 但是返回的 promise 带了 pollingCheck 内声明的箭头函数,其闭包有引用 fn 和 delay,不会导致 pollingCheck 的运行栈无法释放么?
    zyEros
        6
    zyEros  
       2018-09-07 10:24:16 +08:00   ❤️ 1
    其实很简单,你看一些主流的 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
        7
    kingcc  
       2018-09-07 10:30:02 +08:00 via Android
    我说了嘛,简单来说…

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

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

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

    (本人拙见
    pofeng
        13
    pofeng  
    OP
       2018-09-07 11:44:31 +08:00
    @kingcc @zyEros 大概弄明白了,因为 Promise 会再另外一个 Task 运行的的原因所以不会爆栈,而 pollingCheck 的 scope 会释放,不会形成一个很长的链
    leemove
        14
    leemove  
       2018-09-07 12:00:45 +08:00
    哇,这些天天争 Vue,React,Angular 的大手,都被一个 bridgePromise 卡住了...还有 Promise 是走异步事件循环的.
    maplerecall
        15
    maplerecall  
       2018-09-07 12:15:03 +08:00 via Android
    @AnonymousUser es6 已经有尾递归优化了,js 发展还是很快的
    orangemi
        16
    orangemi  
       2018-09-07 12:40:03 +08:00
    问题是为什么不会爆栈,实际上 Promise.then 是把所有的栈都丢掉了,所以不爆栈。
    题主可以尝试使用 new Error().stack 查看左后一次的栈,之前几万次的 stack 都没有了。
    nodejs 一个 tick 间只有一个栈,调用 Promise.then 中间的过程中,已经走到了另外一个 tick。
    leemove
        17
    leemove  
       2018-09-07 12:46:09 +08:00
    @maplerecall js 的尾递归在 V8 上默认是不开启的,在 Node 中也需要对 v8 特殊配置才可以.
    otakustay
        18
    otakustay  
       2018-09-07 12:53:27 +08:00
    异步会清栈,所以 Promise 递归爆不掉
    箭头函数产生的是作用域,不是栈,这个要分清
    Sparetire
        19
    Sparetire  
       2018-09-07 13:54:59 +08:00 via Android
    其实就是函数调用栈转成了异步任务队列了。。同一时刻的内存是有限的,然而即便是无限地递归,转成任务队列这些内存占用也分散在了无限的时间中。。
    xieranmaya
        20
    xieranmaya  
       2018-09-09 18:54:25 +08:00 via Android
    异步递归不是递归,实际上连调用栈都没有,或者说调用栈里就那一个函数
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   1321 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 24ms · UTC 17:36 · PVG 01:36 · LAX 09:36 · JFK 12:36
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.