我需要遍历一些大型数组并从 API 调用中将它们存储到 Backbone 集合中。在不使循环导致界面失去响应的情况下,最好的方法是什么?
由于返回的数据量很大,ajax 请求的返回也会阻塞。我想,我可以将其分割并使用 setTimeout 以较小的块异步运行,但是否有更简单的方法呢?
我认为 web worker 是个好东西,但它需要修改保存在 UI 线程上的一些数据结构。我已经尝试使用它来执行 ajax 调用,但当它将数据返回给 UI 线程时,界面仍然无响应。
提前感谢您
我需要遍历一些大型数组并从 API 调用中将它们存储到 Backbone 集合中。在不使循环导致界面失去响应的情况下,最好的方法是什么?
由于返回的数据量很大,ajax 请求的返回也会阻塞。我想,我可以将其分割并使用 setTimeout 以较小的块异步运行,但是否有更简单的方法呢?
我认为 web worker 是个好东西,但它需要修改保存在 UI 线程上的一些数据结构。我已经尝试使用它来执行 ajax 调用,但当它将数据返回给 UI 线程时,界面仍然无响应。
提前感谢您
你可以选择使用或不使用 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);
与其猜测每次应该分块多少数据,还可以让经过的时间成为每块数据的指引,并在给定的时间间隔内尽可能地处理它们。这样有点类似于自动保证了浏览器的响应能力,无论迭代的计算量有多大。因此,您可以传入毫秒值(或者只是使用智能默认值),而不是传入块大小:// 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需要将所有将在其中运行的代码分离到单独的脚本文件中,但它可以在不影响浏览器其他事件处理的情况下完成运行,而且不用担心在主线程上进行长时间运行过程时可能出现的“无响应脚本”提示,并且不会阻止UI中的事件处理。
.forEach()
风格的回调函数工作,因此可以将同一实用函数用于许多目的。 - jfriend00for..in
对象枚举?创建一个数组,然后执行上述操作?(或者最好提出一个新问题?) - serv-incObject.keys()
来创建数组),因为你不能直接使用for/in
这种方式进行迭代。 - jfriend00window.requestAnimationFrame()
而不是 setTimeout()
更好。这样,你可以确信你的代码不会阻塞,因为浏览器本身告诉你可以进行一些处理。 - dodov这里有一个示例展示了如何执行“异步”循环。它会将迭代“延迟”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上需要加前缀。
setTimeout
函数会将函数排队到回调队列中,在UI有机会完成其操作后再执行。您可以轻松地传入0秒的延迟。 - chris在 @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 (...) return; ...
而不是把所有东西都放在 if
语句里。 - canbax使用下面的代码,您可以使用数组函数(迭代数组)或映射函数(迭代映射)。
此外,现在有一个参数用于在块完成时调用函数(如果需要更新加载消息),以及一个参数用于在处理循环结束时调用函数(必要时进行异步操作完成后执行下一步操作)
//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
});