Javascript内存泄漏问题 - 承诺&递归

时间:2014-09-23 13:27:52

标签: 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,
    };
};

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

虽然代码按预期执行,但浏览器会在一段时间后崩溃。检查任务管理器向我显示标签的进程占用了越来越多的内存。 我一直试图找到原因,但一直无法做到。有人有什么想法吗?

如果我能澄清任何事情,请告诉我。

此致 klmdb

1 个答案:

答案 0 :(得分:1)

好吧,我想我已经深入思考了代码,看来你跳过了许多不必要的箍。主要通过使用两个标准技巧来大大简化代码:

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

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

  • 结果和错误通过承诺链传递,而不是在外部数组中累积。
  • requestIndex(在许多地方)的需求消失了,需要明确关闭其维护。
  • 没有创建延迟对象,这应该有助于使可执行文件减少内存耗尽。
  • 调用new时,
  • RequestManager()现在可选。关于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()返回的承诺,它现在将提供结果数组和(虚假)错误数组,捆绑到js普通对象中。这与runnerMethod保持一致,尽管有错误。

现在,结果是通过承诺链传递的,&#39; storeResults&#39;已经消失了。我看不出有任何理由想要使用storeResults === true以外的任何其他内容。

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

有关详细说明,请参阅代码内注释。

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

修改

阅读(以下评论中)批处理流程的说明和onEachError()等,start()runnerMethod()已在上面进行了编辑。

变更摘要:

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

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

最终,删除响应行为可能是一个值得付出的代价,如果这个版本的内存不足并实际运行完成,这是练习的目标。

要查看未经编辑的代码,请参阅问题的编辑记录