Best way to iterate over an array without blocking the UI

后端 未结 4 1538
谎友^
谎友^ 2020-11-22 13:10

I am needing to iterate over some large arrays and store them in backbone collections from an API call. What is the best way to do this without making the loop cause the int

相关标签:
4条回答
  • 2020-11-22 13:27

    Building on @jfriend00, here is a prototype version:

    if (Array.prototype.forEachAsync == null) {
        Array.prototype.forEachAsync = function forEachAsync(fn, thisArg, maxTimePerChunk, callback) {
            let that = this;
            let args = Array.from(arguments);
    
            let lastArg = args.pop();
    
            if (lastArg instanceof Function) {
                callback = lastArg;
                lastArg = args.pop();
            } else {
                callback = function() {};
            }
            if (Number(lastArg) === lastArg) {
                maxTimePerChunk = lastArg;
                lastArg = args.pop();
            } else {
                maxTimePerChunk = 200;
            }
            if (args.length === 1) {
                thisArg = lastArg;
            } else {
                thisArg = that
            }
    
            let index = 0;
    
            function now() {
                return new Date().getTime();
            }
    
            function doChunk() {
                let startTime = now();
                while (index < that.length && (now() - startTime) <= maxTimePerChunk) {
                    // callback called with args (value, index, array)
                    fn.call(thisArg, that[index], index, that);
                    ++index;
                }
                if (index < that.length) {
                    // set Timeout for async iteration
                    setTimeout(doChunk, 1);
                } else {
                    callback();
                }
            }
    
            doChunk();
        }
    }
    
    0 讨论(0)
  • 2020-11-22 13:39

    Here's a demo of doing this "async" loop. it "delays" iteration for 1ms and within that delay, it gives the UI a chance to do something.

    function asyncLoop(arr, callback) {
        (function loop(i) {
    
            //do stuff here
    
            if (i < arr.Length) {                      //the condition
                setTimeout(function() {loop(++i)}, 1); //rerun when condition is true
            } else { 
                callback();                            //callback when the loop ends
            }
        }(0));                                         //start with 0
    }
    
    asyncLoop(yourArray, function() {
        //do after loop  
    })​;
    
    //anything down here runs while the loop runs
    

    There are alternatives like web workers and the currently proposed setImmediate which afaik, is on IE, with a prefix.

    0 讨论(0)
  • 2020-11-22 13:39

    Thanks so much for this.

    I've updated the code to add some functionality.

    With the code below you can use either the function for arrays (to iterate arrays) or the function for maps (to iterate maps).

    Also, there is now a parameter for a function called when a chunk is completed (helps if you need to update a loading message), as well as a parameter for a function called at the end of processing the loop (necessary for doing the next step after asynchronous operations complete)

    //Iterate Array Asynchronously
    //fn = the function to call while iterating over the array (for loop function call)
    //chunkEndFn (optional, use undefined if not using) = the function to call when the chunk ends, used to update a loading message
    //endFn (optional, use undefined if not using) = called at the end of the async execution
    //last two args are optional
    function iterateArrayAsync(array, fn, chunkEndFn, endFn, maxTimePerChunk, context) {
        context = context || window;
        maxTimePerChunk = maxTimePerChunk || 200;
        var index = 0;
    
        function now() {
            return new Date().getTime();
        }
    
        function doChunk() {
            var startTime = now();
            while (index < array.length && (now() - startTime) <= maxTimePerChunk) {
                // callback called with args (value, index, array)
                fn.call(context,array[index], index, array);
                ++index;
            }
            if((now() - startTime) > maxTimePerChunk && chunkEndFn !== undefined){
                //callback called with args (index, length)
                chunkEndFn.call(context,index,array.length);
            }
            if (index < array.length) {
                // set Timeout for async iteration
                setTimeout(doChunk, 1);
            }
            else if(endFn !== undefined){
                endFn.call(context);
            }
        }    
        doChunk();    
    }
    
    //Usage
    iterateArrayAsync(ourArray,function(value, index, array){
        //runs each iteration of the loop
    },
    function(index,length){
        //runs after every chunk completes, this is optional, use undefined if not using this
    },
    function(){
        //runs after completing the loop, this is optional, use undefined if not using this
    
    });
    
    //Iterate Map Asynchronously
    //fn = the function to call while iterating over the map (for loop function call)
    //chunkEndFn (optional, use undefined if not using) = the function to call when the chunk ends, used to update a loading message
    //endFn (optional, use undefined if not using) = called at the end of the async execution
    //last two args are optional
    function iterateMapAsync(map, fn, chunkEndFn, endFn, maxTimePerChunk, context) {
        var array = Array.from(map.keys());
        context = context || window;
        maxTimePerChunk = maxTimePerChunk || 200;
        var index = 0;
    
        function now() {
            return new Date().getTime();
        }
    
        function doChunk() {
            var startTime = now();
            while (index < array.length && (now() - startTime) <= maxTimePerChunk) {
                // callback called with args (value, key, map)
                fn.call(context,map.get(array[index]), array[index], map);
                ++index;
            }
            if((now() - startTime) > maxTimePerChunk && chunkEndFn !== undefined){
                //callback called with args (index, length)
                chunkEndFn.call(context,index,array.length);
            }
            if (index < array.length) {
                // set Timeout for async iteration
                setTimeout(doChunk, 1);
            }
            else if(endFn !== undefined){
                endFn.call(context);
            }
        }    
        doChunk();
    }
    
    //Usage
    iterateMapAsync(ourMap,function(value, key, map){
        //runs each iteration of the loop
    },
    function(index,length){
        //runs after every chunk completes, this is optional, use undefined if not using this
    },
    function(){
        //runs after completing the loop, this is optional, use undefined if not using this
    
    });
    
    0 讨论(0)
  • 2020-11-22 13:43

    You have a choice of with or without webWorkers:

    Without WebWorkers

    For code that needs to interact with the DOM or with lots of other state in your app, you can't use a webWorker so the usual solution is to break your work into chunks do each chunk of work on a timer. The break between chunks with the timer allows the browser engine to process other events that are going on and will not only allow user input to get processed, but also allow the screen to draw.

    Usually, you can afford to process more than one on each timer which is both more efficient and faster than only doing one per timer. This code gives the UI thread a chance to process any pending UI events between each chunk which will keep the UI active.

    function processLargeArray(array) {
        // set this to whatever number of items you can process at once
        var chunk = 100;
        var index = 0;
        function doChunk() {
            var cnt = chunk;
            while (cnt-- && index < array.length) {
                // process array[index] here
                ++index;
            }
            if (index < array.length) {
                // set Timeout for async iteration
                setTimeout(doChunk, 1);
            }
        }    
        doChunk();    
    }
    
    processLargeArray(veryLargeArray);
    

    Here's a working example of the concept - not this same function, but a different long running process that uses the same setTimeout() idea to test out a probability scenario with a lot of iterations: http://jsfiddle.net/jfriend00/9hCVq/


    You can make the above into a more generic version that calls a callback function like .forEach() does like this:

    // last two args are optional
    function processLargeArrayAsync(array, fn, chunk, context) {
        context = context || window;
        chunk = chunk || 100;
        var index = 0;
        function doChunk() {
            var cnt = chunk;
            while (cnt-- && index < array.length) {
                // callback called with args (value, index, array)
                fn.call(context, array[index], index, array);
                ++index;
            }
            if (index < array.length) {
                // set Timeout for async iteration
                setTimeout(doChunk, 1);
            }
        }    
        doChunk();    
    }
    
    processLargeArrayAsync(veryLargeArray, myCallback, 100);
    

    Rather than guessing how many to chunk at once, it's also possible to let elapsed time be the guide for each chunk and to let it process as many as it can in a given time interval. This somewhat automatically guarantees browser responsiveness regardless of how CPU intensive the iteration is. So, rather than passing in a chunk size, you can pass in a millisecond value (or just use an intelligent default):

    // last two args are optional
    function processLargeArrayAsync(array, fn, maxTimePerChunk, context) {
        context = context || window;
        maxTimePerChunk = maxTimePerChunk || 200;
        var index = 0;
    
        function now() {
            return new Date().getTime();
        }
    
        function doChunk() {
            var startTime = now();
            while (index < array.length && (now() - startTime) <= maxTimePerChunk) {
                // callback called with args (value, index, array)
                fn.call(context, array[index], index, array);
                ++index;
            }
            if (index < array.length) {
                // set Timeout for async iteration
                setTimeout(doChunk, 1);
            }
        }    
        doChunk();    
    }
    
    processLargeArrayAsync(veryLargeArray, myCallback);
    

    With WebWorkers

    If the code in your loop does not need to access the DOM, then it is possible to put all the time consuming code into a webWorker. The webWorker will run independently from the main browser Javascript and then when its done, it can communicate back any results with a postMessage.

    A webWorker requires separating out all the code that will run in the webWorker into a separate script file, but it can run to completion without any worry about blocking the processing of other events in the browser and without the worry about the "unresponsive script" prompt that may come up when doing a long running process on the main thread and without blocking event processing in the UI.

    0 讨论(0)
提交回复
热议问题