简体   繁体   English

JavaScript内存泄漏问题-Promise和递归

[英]Javascript memory leak issue - promises & recursion

I'm having memory issues with this piece of code: 我在这段代码中遇到内存问题:

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,
    };
};

It should be a class to perform batch requests. 它应该是执行批处理请求的类。 The user has the ability to set default request parameters (additional headers etc) and a bunch of batch-settings. 用户可以设置默认请求参数(其他标头等)和大量批处理设置。

While the code performs as expected, the browser crashes after a while. 当代码按预期执行时,浏览器会在一段时间后崩溃。 Checking the task manager shows me the tab's process eats up more and more memory. 检查任务管理器后,我看到选项卡的进程占用了越来越多的内存。 I've been trying to find the reason for this, but have been unable to. 我一直在努力寻找原因,但一直未能做到。 Anyone has any ideas please? 有人有什么想法吗?

Please let me know if I can clearify anything. 请让我知道是否可以清除任何内容。

Regards, klmdb 问候,klmdb

OK, I think I've got my mind round the code and it appears that you jump though a number of unneccessary hoops. 好的,我想我会考虑代码,似乎您跳了很多不必要的圈。 The code can be greatly simplified chiefly through the use of two standard tricks : 主要可以通过使用两个标准技巧来大大简化代码:

  • Use of $.extend() (in two places) which avoids the need to manually loop through objects. 使用$.extend() (在两个地方)可以避免手动循环遍历对象。
  • Use of Array.prototype.reduce() to transform an array into a .then() chain in lieu of "recursion". 使用Array.prototype.reduce()代替“递归”将数组转换成.then()链。

Other features of the version below are : 以下版本的其他功能是:

  • Results and errors are delivered via the promise chain, not accumulated in outer arrays. 结果和错误通过承诺链传递,而不是在外部数组中累积。
  • The need for requestIndex (in many places) disappears, as does the need for explicit closures for its maintenance. requestIndex的需求(在许多地方)都消失了,对其显式关闭进行维护的需求也消失了。
  • No Deferred objects are created, which should help make the executable less memory hungry. 没有创建延迟的对象,这应该有助于减少可执行文件的内存消耗。
  • new is now optional when calling RequestManager() . 现在,在调用RequestManager()new是可选的。 The original code was ambiguous with regard to whether or not new was intended. 关于是否要使用new代码,原始代码不明确。

Here's the simplified version ... 这是简化版...

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
    };
};

untested so may well need debugging 未经测试,因此可能需要调试

The hardest aspect by far is the treatment of errors. 到目前为止,最困难的方面是错误的处理。 The original code has rather odd behaviour in this regard, which I have tried to emulate through the use of faux (non-stopping) errors. 在这方面,原始代码的行为颇为奇怪,我尝试通过使用虚假(不间断)错误来模仿。 Messy-messy but having purged the recursion, I can't think of another way to do it. 凌乱但杂乱无章,但清除了递归后,我想不出另一种方法。

Barring errors on my part, the only difference in behaviour should be in the promise returned by start() , which will now deliver both a results array AND a (faux) errors array, bundled into a js plain object. 除了我自己的错误外,行为上的唯一区别应该在于start()返回的promise,它现在将提供结果数组和(伪)错误数组,并捆绑到js纯对象中。 This is consistent with runnerMethod keeping going despite errors. 尽管出现错误,这与runnerMethod继续运行是一致的。

Now that results are delivered via the promise chain, 'storeResults' has disappeared. 现在,结果是通过承诺链传递的,“ storeResults”已经消失了。 I can't see any reason for ever wanting to run with anything other than storeResults === true . 除了storeResults === true之外,我看不到任何理由要运行。

My only(?) assumptions are that $ is jQuery and that GM_xmlhttpRequest employs jQuery.ajax() and returns (or can be made to return) its jqXHR object. 我唯一的假设是$是jQuery,而GM_xmlhttpRequest使用jQuery.ajax()并返回(或可以使其返回)其jqXHR对象。 This seems reasonable from what I can see. 从我看来,这似乎是合理的。 If the assumption is not valid, then you will need to revert that section of the code. 如果该假设无效,那么您将需要还原该部分代码。

For further explanation see in-code comments. 有关更多说明,请参见代码注释。

When debugged, if it still crashes out, then I would suggest that it is just memory hungry rather than leaky per se . 在调试时,如果仍然崩溃,那么我建议它只是内存不足,而不是泄漏本身

EDIT 编辑

Having read (in comments below) descriptions of the batch process and onEachError() etc, start() and runnerMethod() have been edited above. 阅读(在下面的评论中)批处理过程和onEachError()等的描述后, start()runnerMethod()已在上面进行了编辑。

Summary of changes : 变更摘要:

  • Batch definition: start() now initiates its 8 parallel batches by passing slices of urlAr to runnerMethod(). 批处理定义:start()现在通过将urlAr 切片 urlAr给runnerMethod()来启动其8个并行批处理。
  • requestIndex : is reinstated in a very simple way. requestIndex :以非常简单的方式恢复。

The behaviour of the edited version is similar but not identical to that of the original code in the question. 编辑版本的行为与问题中原始代码的行为相似但不相同。 The difference is that each batch is predefined , not responsive. 区别在于每个批次都是预定义的 ,没有响应。

Ultimately, removing the responsive behaviour may be a price worth paying if this version is less memory hungry and actually runs to completion, which is the object of the exercise. 最终,如果此版本的内存消耗较少并且实际上运行完成,那么消除响应行为可能是值得付出的代价,这是练习的目的。

To see the unedited code, see the question's edit history 要查看未编辑的代码,请参阅问题的编辑历史记录

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM