Nodejs 请教一个 Promise 递归的最佳实践(内存释放)

发布于 1周前 作者 sinazl 来自 nodejs/Nestjs

Nodejs 请教一个 Promise 递归的最佳实践(内存释放)

先上代码

let loading = false;
(async () => {
    if (loading) return;
    loading = true;
    await getAll();
    loading = false;
})()

function getAll(page = 1) {
    return new Promise(async (resolve, reject) => {
        try {
            const body = await getPage(page);
            bigHandle(body); //body 很大 处理完需要及时释放掉
            //  body = null; <--- 尝试过这个 没有用
            if (page < 10) {
                await getAll(++page)
            }
            resolve();
        } catch (error) {
            reject(error);
        }
    })
}

这段代码由于 Promise 嵌套,上一个在等下一个 Promise 完成,上一个无法被释放,最初的 Promise 需要等到 page=10 的时候洋葱模型式的层层返回后释放,pm2 中看到内存一直在飙升。。

如果去掉 Promise,改成 异步回调的形式 一切正常,但是 loading 状态的改变就要写到回调里面去,不是很直观 这里是简化的代码,真实业务中还有一大堆状态 不想都丢到函数的回调去处理 太不优雅了。 请问在使用 Promise 的时候 这种情况的最佳实现是什么?

// node 节点 夜间模式阅读会更舒服 日间模式 太黑了。。


21 回复

为什么不直接 Promise.all 展开呢。并没有看到或者说明下一次 Promise 需要前面做什么。如果需要前者可以链式调用呀,可以及时释放。


因为第二页需要第一页的数据,所以需要递归调用。 而且需要控制频率,所以不能 Promise.all 并发一把梭

把 body 变量放外面?

promise 没学明白就拿 async await 来写代码就这样了

用 new promise 来包别的 promise 已经是反模式了,还再加上 async await

这代码没治了,从头重写吧

Promise 和 async 混用感觉很奇怪…确实按#1 来说,body 在处理 getAll(page)时是不必要的。
<br>getPage(page)<br>.then((body) =&gt; {<br> bigHandle(body)<br>})<br>.then(() =&gt; {<br> getAll(++page)<br>}, (error) =&gt; {<br> //...<br>})<br>.<br>

写 node 从不关心内存占用……感觉是伪命题。

但你这个可能可以这么解决。

<br>var body;<br>for (var i = 1; i &lt; 10; i ++) {<br> body = await getPage(page);<br> bigHandle(body);<br>}<br>

为啥想不开要递归……

感觉不是单纯的 body 问题。body 再怎么大也就 1m 的纯文本。内存几十兆几十兆的涨。
主要是 Promise 整个堆栈没有释放,这个才是内存爆炸的主要原因。
但是 递归的 Promise 怎么合理的释放上一个 Promise 感觉这是个悖论了……
所以来问问有没有这类问题的最佳实践。

难道只能回到 回调地狱 来处理了么~

同意 说的。
getAll 完全可以不用 Promise,也不要用递归。里面写个 for 循环就好了。有了 await /async,完全可以把 node 异步当成同步来开发。

let loading = false;
(async () => {
if (loading) return;
loading = true;
for (let i = 0; i < 10; i++) {
let body = await getPage(page);
bigHandle(body); //body 很大 处理完需要及时释放掉
}
loading = false;
})()

emmm,Page 数量是提前可以知道的么? 提前的话只需要顺序处理就可以了啊。还可以加 Delay 随意。Reduce 或者直接 for await 不行么 哈哈哈。

要么 Promise 要么 async/await 混用头一次见

async 返回本身就是 promise 啊,总觉得代码怪怪的。

有流程控制的时候可以试试 promise chain
[ …Array(10).keys()].map(page => getPage(page)).reduce((pc, func) => {
return pc.then(() => new Promise(resolve => func(resolve)), Promise.resolve());
});

https://asciinema.org/a/dKWCuCHxZ3vkxOaifb5Rlksxj
抛点砖。body = null 是有用的,能让内存使用减少一半,但是还是非常占用。手动触发 GC 能让内存占用维持在稳定水平

let loading = false;

while(page<10){
if (loading) return;

loading = true;

await getAll();
loading = false;
}

有老哥指点一下 Promise 和 async/await 怎么一起使用吗, 上面的回复让我看懵了

为啥不用循环,要用地递归来写一个自己若干天之后都不能理解的代码

在一个没法 reify 活动记录确保显式释放又没 proper tail call 保证的玩意儿里瞎搞?想多了。
呵、呵:
https://github.com/nodejs/CTC/issues/3
有点意义的例子:
https://srfi.schemers.org/srfi-45/srfi-45.html

怎么又是 new promise 又是 async 的, async 就是返回 promise 的语法啊
递归的话肯定要尾递归的,我觉得这么写就行了
let loading = false;
(async () => {
if (loading) return;
loading = true;
await getAll();
loading = false;
})()

async function getAll(page = 1) {
const body = await getPage(page);
bigHandle(body); //body 很大 处理完需要及时释放掉
if (page < 10) {
return getAll(++page)
}
}

不要用递归,展开吧。。

在Node.js中处理Promise递归时,确保内存释放的最佳实践涉及正确管理异步链,避免内存泄漏和未处理的Promise。以下是一个使用Promise递归处理异步操作(如读取文件或进行API调用)时内存管理的示例。

function recursiveFetch(url, depth = 0, maxDepth = 5) {
    if (depth > maxDepth) return Promise.resolve();

    return fetch(url)
        .then(response => {
            if (!response.ok) throw new Error('Network response was not ok');
            return response.json();
        })
        .then(data => {
            console.log(`Depth ${depth}:`, data);
            // 假设每个响应对象中有一个 'next' URL 用于递归
            if (data.next) {
                return recursiveFetch(data.next, depth + 1, maxDepth);
            }
        })
        .catch(error => {
            console.error('Error:', error);
        })
        .finally(() => {
            // 在每个递归层次结束时清理资源(如果有必要)
            // 例如:clearTimeout(someTimeoutId); 或其他资源释放操作
            console.log(`Depth ${depth} finished`);
        });
}

// 开始递归调用
recursiveFetch('https://api.example.com/data');

在上述代码中:

  1. fetch 函数用于模拟异步请求。
  2. 递归深度通过 depth 参数控制,并在达到 maxDepth 时停止。
  3. finally 块确保无论成功还是失败,都会在递归的每一层结束时执行,用于资源清理(如本例中的日志记录)。
  4. 错误处理通过 catch 块实现,避免未处理的Promise拒绝。

这样设计递归Promise可以确保内存有效管理,避免泄漏。

回到顶部