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

时间:2020-12-29 01:50:08

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.

我认为web worker会很好,但是它需要修改在UI线程上保存的一些数据结构。我尝试过使用它来执行ajax调用,但是当它将数据返回给UI线程时,仍然有一段时间接口没有响应。

Thanks in advance

谢谢提前

3 个解决方案

#1


67  

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.

对于需要与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 separate out all 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.

网络系统需要分离出网络系统中的所有代码将运行到一个单独的脚本文件,但它可以运行完成没有任何担心阻塞其他事件的处理在浏览器中,不担心可能出现的“无响应脚本”提示时做一个长时间运行的流程在主线程。

#2


5  

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.

这里有一个“async”循环的演示。它“延迟”了1ms的迭代,并且在这个延迟之内,它给了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 workers and the currently proposed setImmediate which afaik, is on IE, with a prefix.

有一些替代方法,比如web workers,以及当前提议的setimmediaafaik,在IE上有前缀。

#3


0  

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

#1


67  

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.

对于需要与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 separate out all 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.

网络系统需要分离出网络系统中的所有代码将运行到一个单独的脚本文件,但它可以运行完成没有任何担心阻塞其他事件的处理在浏览器中,不担心可能出现的“无响应脚本”提示时做一个长时间运行的流程在主线程。

#2


5  

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.

这里有一个“async”循环的演示。它“延迟”了1ms的迭代,并且在这个延迟之内,它给了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 workers and the currently proposed setImmediate which afaik, is on IE, with a prefix.

有一些替代方法,比如web workers,以及当前提议的setimmediaafaik,在IE上有前缀。

#3


0  

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