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

IT技术 javascript ajax arrays backbone.js settimeout
2021-01-27 12:11:12

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

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

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

提前致谢

4个回答

您可以选择使用或不使用 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 中的事件处理时,可能会出现这种情况。

添加了一个通过.forEach()样式回调工作的更通用的版本,因此相同的实用程序函数可用于多种用途。
2021-03-12 12:11:12
添加了另一个通用版本,该版本按时间而不是数量进行分块,因此它将根据迭代所需的时间调整自己的分块大小(保持浏览器响应)。
2021-03-12 12:11:12
@user - 是的,您必须先创建数组(您可以Object.keys()用来创建数组),因为您不能直接使用for/in.
2021-03-16 12:11:12
这将如何应用于for..in对象枚举?创建一个数组,那么上面的呢?(或者最好问一个新问题?)
2021-03-22 12:11:12
我认为使用window.requestAnimationFrame()而不是setTimeout()a 好多了。这样,您绝对可以确定您的代码不会被阻塞,因为浏览器本身会告诉您进行一些处理很酷。
2021-04-07 12:11:12

是执行此“异步”循环的演示它“延迟”迭代 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

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

1ms 延迟不是 UI 更新的原因。setTimeout函数会将回调队列中的函数排入队列,在 UI 有机会执行其操作后,回调队列将被接收。您可以轻松地传递 0 秒延迟。
2021-03-11 12:11:12
不如之前的答案好,因为它在每个元素之后调用 setTimeout
2021-04-03 12:11:12

以@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();
    }
}
而不是把所有东西都放在里面,if我更喜欢if (...) return; ...
2021-04-10 12:11:12

非常感谢。

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

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

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

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

});
async我猜调用这个函数会产生误导。操作仍然在同一个线程上同步。
2021-04-09 12:11:12