Javascript 内存泄漏问题 - promise 和递归

标签 javascript recursion memory-leaks deferred

我在这段代码中遇到了内存问题:

var RequestManager = function(customRequestArgs){

    var requestManager = this;

    this.customRequestArgs = customRequestArgs              || [];



    this.CustomRequest = function(url, data){

        var requestDeferred = $.Deferred();

        // set default xmlRequestArgs
        var xmlRequestArgs = {
            method  : "GET",
            url     : url,
            onload  : function(response) {
                requestDeferred.resolve(response.responseText);
            },
            onerror : function(response){
                requestDeferred.reject('xmlRequest failed', response);
            }
        };
        // set custom xmlRequestArgs
        var i;
        for(i in requestManager.customRequestArgs){
            if(requestManager.customRequestArgs.hasOwnProperty(i)){
                xmlRequestArgs[i] = requestManager.customRequestArgs[i];
            }
        }

        // append data, depending on method
        var d = [];
        for(i in data){
            if(data.hasOwnProperty(i)){
                d.push(i+'='+encodeURIComponent(data[i]));
            }
        }
        var dataString = d.join('&');

        if(xmlRequestArgs.method.toLowerCase() === 'get'){
            if(url.indexOf('?')>=0){
                xmlRequestArgs.url = url+dataString;
            }
            else{
                xmlRequestArgs.url = url+'?'+dataString;
            }
        }
        if(xmlRequestArgs.method.toLowerCase() === 'post'){
            xmlRequestArgs.data = dataString;
        }


        // run request
        GM_xmlhttpRequest(xmlRequestArgs);

        return requestDeferred;
    };

    this.BatchRequestRunner = function(args){

        var maxParallelRequests = args.maxParallelRequests || 8;

        var onEachStart         = args.onEachStart              || function(requestIndex, url){return undefined;};          // must return undefined or loader promise (i.e. for cached results)
        var onEachSuccess       = args.onEachSuccess            || function(result, requestIndex, url){return result;};     // must return result or promise that resolves to result
        var onEachError         = args.onEachError              || function(error, requestIndex, url){return error;};       // must return error or promise that resolves to error

        var urlAr               = args.urlAr                    || [];

        var storeResults        = args.storeResults             || false;

        var reversedUrlArClone  = urlAr.slice(0).reverse();
        var deferredAr          = [];
        var resultAr            = [];
        var errorAr             = [];


        var runnerMethod = function(){

            if(reversedUrlArClone.length > 0){

                // get request url
                var url = reversedUrlArClone.pop();

                // get urlIndex (i-th url in urlAr)
                var requestIndex = urlAr.length - reversedUrlArClone.length - 1;


                // run onEachStart
                $.when(onEachStart(requestIndex, url)).then(function(loaderPromise){

                    if(loaderPromise === undefined){

                        // set loaderPromise
                        loaderPromise = requestManager.CustomRequest(url);

                    }

                    var generateOnSuccess = function(requestIndex){
                        return function(result){


                            $.when(onEachSuccess(result, requestIndex, url)).then(function(result){

                                // store result
                                if(storeResults){
                                    resultAr[requestIndex] = result;
                                }

                                // resolve deferredAr[requestIndex]
                                deferredAr[requestIndex].resolve();

                                // start runnerMethod for next request
                                runnerMethod();

                            });

                        };
                    };
                    var generateOnError = function(requestIndex){
                        return function(error){

                            $.when(onEachError(error, requestIndex, url)).then(function(error){

                                // store error
                                errorAr[requestIndex] = error;

                                // reject deferredAr[requestIndex]
                                deferredAr[requestIndex].reject();

                                // start runnerMethod for next request
                                runnerMethod(); 


                            });

                        };
                    };

                    // handle loader
                    loaderPromise.done(generateOnSuccess(requestIndex));
                    loaderPromise.fail(generateOnError(requestIndex));

                });

            }

        };

        var startParallelRequestThread = function(){
            runnerMethod();
        };

        var start = function(){
            var i,
                runnerDeferred  = $.Deferred();

            // setup deferredAr
            for(i=0;i<urlAr.length;i++){
                deferredAr.push($.Deferred());
            }

            // setup onSuccess
            $.when.apply($, deferredAr)
            .done(function(){
                runnerDeferred.resolve(resultAr);
            })
            // setup onError
            .fail(function(){
                runnerDeferred.reject(errorAr);
            });

            // start requestThreads
            for(i=0;i<maxParallelRequests;i++){
                startParallelRequestThread();
            }

            return runnerDeferred;
        };


        return {
            start       : start
        };

    };



    return {
        BatchRequestRunner  : this.BatchRequestRunner,
        CustomRequest       : this.CustomRequest,
    };
};

它应该是一个执行批量请求的类。用户可以设置默认请求参数(附加 header 等)和一堆批处理设置。

虽然代码按预期执行,但浏览器在一段时间后崩溃。检查任务管理器显示选项卡的进程占用了越来越多的内存。 我一直试图寻找其中的原因,但一直未能找到。请问有人有什么想法吗?

如果我可以澄清任何问题,请告诉我。

问候, klmdb

最佳答案

好吧,我想我已经仔细考虑了代码,而且看起来您跳过了一些不必要的麻烦。主要通过使用两个标准技巧可以大大简化代码:

  • 使用 $.extend()(在两个地方),避免了手动循环对象的需要。
  • 使用 Array.prototype.reduce() 将数组转换为 .then() 链来代替“递归”。

以下版本的其他功能包括:

  • 结果和错误通过 Promise 链传递,而不是累积在外部数组中。
  • requestIndex 的需求(在很多地方)消失了,维护它的显式闭包的需求也消失了。
  • 不会创建延迟对象,这应该有助于减少可执行文件对内存的占用。
  • 现在,在调用 RequestManager() 时,
  • new 是可选的。原始代码对于 new 是否有意为之并不明确。

这是简化版本...

var RequestManager = function(customRequestArgs) {
    var CustomRequest = function(url, data) {
        //GM_xmlhttpRequest is assumed to call $.ajax() (or one of its shorthand methods) and return a jqXHR object
        return GM_xmlhttpRequest($.extend({ //$.extend() replaces several lines of original code
            method: "GET",
            url: url,
            data: data
        }, customRequestArgs || {})).then(function(response) {
            return response.responseText;
        }, function(jqXHR, textStatus, errorThrown) {
            return ('xmlRequest failed: ' + textStatus);
        });
    };
    //Defaults are best defined (once per RequestManager) as an object, which can be extended with $.extend().
    var batchRequestDefaults = {
        maxParallelRequests: 8,
        onEachStart: function(url) { return undefined; }, // must return undefined or loader promise (i.e. for cached results)
        onEachSuccess: function(result, url){ return result; }, // must return result or promise that resolves to result
        onEachError: function(error, url){ return error; }, // must return error or promise that resolves to error.
        urlAr: [],
        storeResults: false
    };
    var BatchRequestRunner = function(args) {
        args = $.extend({}, batchRequestDefaults, args); //$.extend() replaces several lines of original code
        function runnerMethod(index, urlAr) {
            //Note recursion is avoided here by the use of .reduce() to build a flat .then() chain.
            return urlAr.reverse().reduce(function(promise, url) {
                var requestIndex = index++;
                return promise.then(function(result1) {
                    return $.when(args.onEachStart(requestIndex, url)).then(function(p) {
                        return (p === undefined) ? CustomRequest(url) : p;
                    }).then(function(result2) {
                        args.onEachSuccess(result2, requestIndex, url);
                        // No return value is necessary as result2 is assumed 
                        // to be fully handled by onEachSuccess(),
                        // so doesn't need to be passed down the promise chain.
                    }, function(error) {
                        // This is messy but : 
                        // (a) is consistent with the stated rules for writing onEachError() functions.
                        // (b) maintains the original code's behaviour of keeping going despite an error.
                        // This is achieved by returning a resolved promise from this error handler.
                        return $.when(args.onEachError(error, requestIndex, url)).then(function(error) {
                            return $.when(); //resolved promise
                        });
                    });
               });
            }, $.when());
        }
        var start = function() {
            // start requestThreads
            var i, promises = [],
                pitch = Math.ceil(args.urlAr / args.maxParallelRequests),
                startIndex, endIndex;
            for(i=0; i<args.maxParallelRequests; i++) {
                startIndex = pitch * i;
                endIndex = pitch * (i + 1) - 1;
                promises.push(runnerMethod(startIndex, args.urlAr.slice(startIndex, endIndex)));
            }
            // Note: Results and errors are assumed to be fully handled by onEachSuccess() and onEachError() so do not need to be handled here or passed on down the promise chain.
            return $.when.apply(null, promises);
        };
        return {
            start: start
        };
    };
    return {
        BatchRequestRunner: BatchRequestRunner,
        CustomRequest: CustomRequest
    };
};

未经测试,因此可能需要调试

到目前为止,最难的方面是错误的处理。原始代码在这方面有相当奇怪的行为,我尝试通过使用人造(不间断)错误来模拟。凌乱,但清除了递归后,我想不出另一种方法来做到这一点。

除了我的错误之外,行为上的唯一区别应该是 start() 返回的 promise ,它现在将提供一个结果数组和一个(人造)错误数组,捆绑到一个 js 普通对象。这与 runnerMethod 尽管出现错误仍继续运行是一致的。

现在结果是通过 promise 链传递的,“storeResults”已经消失了。我看不出有任何理由想要使用 storeResults === true 之外的任何东西来运行。

我唯一的(?)假设是 $ 是 jQuery 并且 GM_xmlhttpRequest 使用 jQuery.ajax() 并返回(或者可以是返回)其jqXHR对象。从我看来,这似乎是合理的。如果假设无效,那么您将需要恢复该代码部分。

有关进一步的说明,请参阅代码中的注释。

调试时,如果它仍然崩溃,那么我建议它只是内存不足,而不是泄漏本身

编辑

阅读(在下面的评论中)批处理和 onEachError() 等的描述后,start()runnerMethod() 已经上面已经编辑过。

变更摘要:

  • 批处理定义:start() 现在通过将 urlAr切片传递给 runnerMethod() 来启动 8 个并行批处理。
  • requestIndex:以非常简单的方式恢复。

编辑版本的行为与问题中的原始代码类似但不完全相同。不同之处在于每个批处理都是预定义的,而不是响应式的。

最终,如果此版本内存消耗较少并且实际上运行完成(这就是练习的目标),则删除响应行为可能是值得付出的代价。

要查看未编辑的代码,请查看问题的编辑历史记录

关于Javascript 内存泄漏问题 - promise 和递归,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/25996400/

相关文章:

java - 递归法

Java ExecutorService 解决递归斐波那契数列

c - 最小的 bison/flex 生成的代码有内存泄漏

javascript - 德克西JS : very large IndexedDB but empty tables

javascript - 在 Three.js 中制作菜单

javascript - Heroku ENV Vars 返回未定义但已定义

c++ - 函数结束后子节点的树递归c++缺失值

android - LeakCanary 没有提供足够的信息来识别泄漏

java - tomcat上特定应用程序的线程列表

javascript - 从字符串中获取特定值/参数(keypare)