Javascript 在不阻塞 UI 的情况下迭代数组的最佳方法

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/10344498/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-24 00:58:17  来源:igfitidea点击:

Best way to iterate over an array without blocking the UI

javascriptajaxarraysbackbone.jssettimeout

提问by georgephillips

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 interface to become unresponsive?

我需要遍历一些大型数组并将它们存储在来自 API 调用的主干集合中。在不使循环导致界面无响应的情况下执行此操作的最佳方法是什么?

The return of the ajax request also blocks as the data returned is so large. I figure that I could split it up and use setTimeout to make it run asynchronously in smaller chunks but is there an easier way to do this.

由于返回的数据如此之大,ajax 请求的返回也会阻塞。我认为我可以将其拆分并使用 setTimeout 使其以较小的块异步运行,但是有没有更简单的方法来做到这一点。

I thought a web worker would be good but it needs to alter some data structures saved on the UI thread. I have tried using this to do the ajax call but when it returns the data to the UI thread there is still a time when the interface is unresponsive.

我认为网络工作者会很好,但它需要更改保存在 UI 线程上的一些数据结构。我曾尝试使用它来执行 ajax 调用,但是当它向 UI 线程返回数据时,仍有一段时间界面没有响应。

Thanks in advance

提前致谢

回答by jfriend00

You have a choice of with or without webWorkers:

您可以选择使用或不使用 webWorker:

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.

对于需要与 DOM 或应用程序中的许多其他状态进行交互的代码,您不能使用 webWorker,因此通常的解决方案是将您的工作分解为多个块,在计时器上完成每个工作块。定时器块之间的中断允许浏览器引擎处理正在发生的其他事件,不仅允许处理用户输入,还允许屏幕绘制。

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.

通常,您可以在每个计时器上处理多个,这比每个计时器只处理一个更有效和更快。此代码使 UI 线程有机会处理每个块之间的任何挂起的 UI 事件,这将使 UI 保持活动状态。

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/

这是这个概念的一个工作示例 - 不是这个相同的函数,而是一个不同的长时间运行的过程,它使用相同的setTimeout()想法来测试具有大量迭代的概率场景: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:

你可以把上面的代码变成一个更通用的版本,它调用一个回调函数.forEach(),就像这样:

// 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):

与其猜测一次要分多少块,还可以让经过的时间作为每个块的指南,并让它在给定的时间间隔内处理尽可能多的块。无论迭代的 CPU 密集程度如何,这都会在一定程度上自动保证浏览器响应能力。因此,您可以传入一个毫秒值(或仅使用智能默认值),而不是传入块大小:

// 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.

如果循环中的代码不需要访问 DOM,那么可以将所有耗时的代码放入一个 webWorker 中。webWorker 将独立于主浏览器 Javascript 运行,然后当它完成时,它可以用 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.

一个 webWorker 需要将所有将在 webWorker 中运行的代码分离到一个单独的脚本文件中,但它可以运行到完成而无需担心阻止浏览器中其他事件的处理,也无需担心“无响应脚本”提示当在主线程上执行长时间运行的进程并且不阻塞 UI 中的事件处理时,可能会出现这种情况。

回答by Joseph

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

是执行此“异步”循环的演示。它“延迟”迭代 1 毫秒,在此延迟内,它让 UI 有机会做一些事情。

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 workersand the currently proposed setImmediatewhich afaik, is on IE, with a prefix.

有一些替代方案,如web workers当前提议的 setImmediate,它在 IE 上,带有前缀。

回答by cjbarth

Building on @jfriend00, here is a prototype version:

以@jfriend00 为基础,这是一个原型版本:

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

回答by wootowl

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

});