这段基于 Promise 的递归为什么不会爆栈?求 JS 大拿 - V2EX
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 6993 次点击
    这是一个创建于 2599 天前的主题,其中的信息可能已经有所发展或是发生改变。

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

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

    代码如下:

    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();
    ```
    AV1
        9
    AV1  
       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     Solana     2750 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 25ms UTC 13:03 PVG 21:03 LAX 06:03 JFK 09:03
    Do have faith in what you're doing.
    ubao msn snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86