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

我需要迭代一些大型数组,并将它们存储在来自 API 调用的主干集合中。在不使循环导致接口无响应的情况下,最好的方法是什么?

由于返回的数据太大,ajax 请求的返回也会被阻塞。我认为我可以把它分割开来,使用 setTimeout 来使它在更小的块中异步运行,但是有更简单的方法来做到这一点吗。

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

先谢谢你

34651 次浏览

你可以选择是否使用 webWorkers:

没有网络工作者

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

通常情况下,您可以在每个计时器上处理多个计时器,这比每个计时器只处理一个计时器更有效、更快。这段代码使 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);

下面是这个概念的一个工作示例——不是这个相同的函数,而是一个不同的长时间运行的流程,它使用相同的 setTimeout()思想来测试一个具有大量迭代的概率场景: http://jsfiddle.net/jfriend00/9hCVq/


您可以将上面的代码转换成更通用的版本,像 .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);

与其猜测一次要处理多少块,还可以让运行时间作为每个块的指导,并让它在给定的时间间隔内处理尽可能多的块。这在某种程度上自动保证了浏览器的响应性,而不管迭代的 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);

与网络工作者

如果循环中的代码不需要访问 DOM,那么可以将所有耗时的代码放到 webWorker 中。WebWorker 将独立于主浏览器 Javascript 运行,当它运行完成时,它可以通过 postMessage 反馈任何结果。

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

基于@jfriend 00,这里有一个原型版本:

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

非常感谢。

我更新了代码以添加一些功能。

使用下面的代码,您可以使用数组函数(迭代数组)或映射函数(迭代映射)。

此外,现在还有一个参数用于在块完成时调用函数(如果需要更新加载消息,这将有所帮助) ,还有一个参数用于在处理循环结束时调用的函数(在异步操作完成后执行下一步所必需的)

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


});