Как остановить утечку памяти с помощью рекурсивного javascript promises?

Как создать рекурсивную цепочку Javascript promises с библиотекой Q? Следующий код не может быть завершен в Chrome:

<html>
    <script src="q.js" type="text/javascript"></script>
    <script type="text/javascript">
        //Don't keep track of a promises stack for debugging
        //Reduces memory usage when recursing promises
        Q.longStackJumpLimit = 0;

        function do_stuff(count) {
            if (count==1000000) {
                return;
            }

            if (count%10000 == 0){
                console.log( count );
            }

            return Q.delay(1).then(function() {
                return do_stuff(count+1);
            });
        }

        do_stuff(0)
        .then(function() {
            console.log("Done");
        });
    </script>
</html>

Ответ 1

Это не приведет к переполнению стека, потому что promises сломает стек, но это приведет к утечке памяти. Если вы запустите этот же код в node.js, вы получите сообщение об ошибке:

FATAL ERROR: CALL_AND_RETRY_2 Allocation failed - process out of memory

Что здесь происходит, так это то, что создается действительно длинная цепочка вложенных promises, каждая из которых ждет следующего. То, что вам нужно сделать, это найти способ сгладить эту цепочку, чтобы получить только одно обещание высшего уровня, которое возвращается, ожидая самого внутреннего обещания, которое в настоящее время представляет собой реальную работу.

разрыв цепи

Самое простое решение - построить новое обещание на верхнем уровне и использовать его для разрыва рекурсии:

var Promise = require('promise');

function delay(timeout) {
    return new Promise(function (resolve) {
        setTimeout(resolve, timeout);
    });
}

function do_stuff(count) {
    return new Promise(function (resolve, reject) {
        function doStuffRecursion(count) {
            if (count==1000000) {
                return resolve();
            }

            if (count%10000 == 0){
                console.log( count );
            }

            delay(1).then(function() {
                doStuffRecursion(count+1);
            }).done(null, reject);
        }
        doStuffRecursion(count);
    });
}

do_stuff(0).then(function() {
    console.log("Done");
});

Хотя это решение несколько неэлегантно, вы можете быть уверены, что оно будет работать во всех реализациях обещаний.

, то/обещание теперь поддерживает рекурсию хвоста

Некоторые реализации обещаний (например promise из npm, которые вы можете скачать как автономную библиотеку из https://www.promisejs.org/) правильно определить этот случай и свернуть цепочку promises в одно обещание. Это работает, если вы не держите ссылку на обещание, возвращенное функцией верхнего уровня (т.е. Наберите .then на нем немедленно, не держите его).

Хорошо:

var Promise = require('promise');

function delay(timeout) {
    return new Promise(function (resolve) {
        setTimeout(resolve, timeout);
    });
}

function do_stuff(count) {
    if (count==1000000) {
        return;
    }

    if (count%10000 == 0){
        console.log( count );
    }

    return delay(1).then(function() {
        return do_stuff(count+1);
    });
}

do_stuff(0).then(function() {
    console.log("Done");
});

Плохо:

var Promise = require('promise');

function delay(timeout) {
    return new Promise(function (resolve) {
        setTimeout(resolve, timeout);
    });
}

function do_stuff(count) {
    if (count==1000000) {
        return;
    }

    if (count%10000 == 0){
        console.log( count );
    }

    return delay(1).then(function() {
        return do_stuff(count+1);
    });
}

var thisReferenceWillPreventGarbageCollection = do_stuff(0);

thisReferenceWillPreventGarbageCollection.then(function() {
    console.log("Done");
});

К сожалению, ни одна из встроенных реализаций обещаний не имеет такой оптимизации, и никто не планирует ее реализовывать.

Ответ 2

Ниже приведена самая простая реализация того, что вы пытаетесь сделать, если это работает, тогда возникает проблема с библиотекой q, в противном случае возникают некоторые проблемы с javascript:

<html>
    <script type="text/javascript">
        function do_stuff(count) {
            if (count==1000000) {
                return done();
            }

            if (count%1000 == 0){
                console.log( count );
            }

            return setTimeout(function() { do_stuff(count+1); }, 0);
        }

        do_stuff(0);

        function done() {
            console.log("Done");
        };
    </script>
</html>