我需要遍历一些大型数组并将它们存储在 API 调用的主干集合中。在不使循环导致界面无响应的情况下执行此操作的最佳方法是什么?
由于返回的数据太大,ajax 请求的返回也会阻塞。我认为我可以将其拆分并使用 setTimeout 使其以较小的块异步运行,但有更简单的方法可以做到这一点。
我认为网络工作者会很好,但它需要更改保存在 UI 线程上的一些数据结构。我已经尝试使用它来进行 ajax 调用,但是当它返回数据到 UI 线程时,界面仍然有一段时间没有响应。
提前致谢
您可以选择使用或不使用 webWorkers:
没有 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);
使用 WebWorkers
如果循环中的代码不需要访问 DOM,那么可以将所有耗时的代码放入 webWorker。 webWorker 将独立于主浏览器 Javascript 运行,然后当它完成后,它可以使用 postMessage 将任何结果传回。
webWorker 需要将在 webWorker 中运行的所有代码分离到一个单独的脚本文件中,但它可以运行完成,而无需担心阻塞浏览器中其他事件的处理,也无需担心“无响应脚本”提示在主线程上执行长时间运行的进程并且没有阻塞 UI 中的事件处理时可能会出现这种情况。
Here's a demo 执行此“异步”循环。它“延迟”迭代 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 和 currently proposed setImmediate 之类的替代方法,其中 afaik 是 on IE,带有前缀。
setTimeout
函数将在回调队列中对函数进行排队,在 UI 有机会执行它的操作后将被拾取。您可以轻松地通过 0 秒延迟。
基于@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; ...
非常感谢。
我已经更新了代码以添加一些功能。
使用下面的代码,您可以使用数组函数(迭代数组)或映射函数(迭代映射)。
此外,现在有一个用于在块完成时调用的函数的参数(如果您需要更新加载消息会有所帮助),以及在处理循环结束时调用的函数的参数(执行下一步所必需的)异步操作完成后的步骤)
//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
会产生误导。这些操作在同一个线程上仍然是同步的。
.forEach()
样式回调工作,因此可以将相同的实用程序函数用于多种用途。for..in
对象枚举?创建一个数组,那么上面呢? (或者更好地问一个新问题?)Object.keys()
创建数组),因为您不能直接使用for/in
进行迭代。window.requestAnimationFrame()
而不是setTimeout()
要好得多。这样,您绝对可以确定您的代码没有阻塞,因为 浏览器本身 告诉您进行一些处理很酷。