我需要一个等待异步调用然后继续的循环。就像是:
for ( /* ... */ ) {
someFunction(param1, praram2, function(result) {
// Okay, for cycle could continue
})
}
alert("For cycle ended");
我该怎么办?你有什么想法?
Answers:
如果阻止脚本和浏览器,则无法在JavaScript中混合使用同步和异步。
您需要在此处采用完整的事件驱动方式,幸运的是我们可以将丑陋的东西藏起来。
编辑:更新了代码。
function asyncLoop(iterations, func, callback) {
var index = 0;
var done = false;
var loop = {
next: function() {
if (done) {
return;
}
if (index < iterations) {
index++;
func(loop);
} else {
done = true;
callback();
}
},
iteration: function() {
return index - 1;
},
break: function() {
done = true;
callback();
}
};
loop.next();
return loop;
}
这将为我们提供一个异步方法loop,您当然可以进一步修改它,例如使用一个检查循环条件的函数等。
现在进行测试:
function someFunction(a, b, callback) {
console.log('Hey doing some stuff!');
callback();
}
asyncLoop(10, function(loop) {
someFunction(1, 2, function(result) {
// log the iteration
console.log(loop.iteration());
// Okay, for cycle could continue
loop.next();
})},
function(){console.log('cycle ended')}
);
并输出:
Hey doing some stuff!
0
Hey doing some stuff!
1
Hey doing some stuff!
2
Hey doing some stuff!
3
Hey doing some stuff!
4
Hey doing some stuff!
5
Hey doing some stuff!
6
Hey doing some stuff!
7
Hey doing some stuff!
8
Hey doing some stuff!
9
cycle ended
loop.break()打算做什么感到困惑?如果您想要的话,只是一种强制方法?
我简化了这个:
功能:
var asyncLoop = function(o){
var i=-1;
var loop = function(){
i++;
if(i==o.length){o.callback(); return;}
o.functionToLoop(loop, i);
}
loop();//init
}
用法:
asyncLoop({
length : 5,
functionToLoop : function(loop, i){
setTimeout(function(){
document.write('Iteration ' + i + ' <br>');
loop();
},1000);
},
callback : function(){
document.write('All done!');
}
});
假设您只需要对集合进行一次异步调用,则@Ivo建议的更干净的替代方法将是“ 异步方法队列”。
(有关详细说明,请参阅达斯汀·迪亚兹(Dustin Diaz)的这篇文章)
function Queue() {
this._methods = [];
this._response = null;
this._flushed = false;
}
(function(Q){
Q.add = function (fn) {
if (this._flushed) fn(this._response);
else this._methods.push(fn);
}
Q.flush = function (response) {
if (this._flushed) return;
this._response = response;
while (this._methods[0]) {
this._methods.shift()(response);
}
this._flushed = true;
}
})(Queue.prototype);
您只需创建一个新的实例Queue,添加所需的回调,然后使用异步响应刷新队列。
var queue = new Queue();
queue.add(function(results){
for (var result in results) {
// normal loop operation here
}
});
someFunction(param1, param2, function(results) {
queue.flush(results);
}
这种模式的另一个好处是您可以向队列添加多个功能,而不仅仅是一个功能。
如果您有一个包含迭代器函数的对象,则可以在后台添加对此队列的支持,并编写看起来是同步的代码,但实际上不是:
MyClass.each(function(result){ ... })
只需编写each即可将匿名函数放入队列中,而不是立即执行它,然后在异步调用完成后刷新队列。这是一个非常简单而强大的设计模式。
PS:如果您使用的是jQuery,则已经有一个异步方法队列可供使用,称为jQuery.Deferred。
someFunction从而延迟其余的循环,您的模式将建立一个函数列表,这些函数将按顺序执行并全部接收结果一个其他函数调用。这是一个很好的模式,但我认为它与所讨论的问题不符。
setTimeout。如果代码的执行速度比预期的要快,则可能会导致意外行为。
setTimeout如果回调仅花费一半的时间,那么我会以哪种方式冒意外行为的风险,那么代码又可以更快地执行...那么,这有什么意义呢?“循环”中的“代码”仍然井井有条,如果您在完整的回调之前在它之外进行了一些工作,您已经在打电话给麻烦了,但是再说一次它是单线程的,我很难提出一个setTimeout会破坏某些东西的场景,而不会进行进一步的错误设计。
还要看看这个出色的库caolan / async。你的for循环可以很容易地使用来完成mapSeries或系列。
如果您的示例中包含更多详细信息,我可以发布一些示例代码。
我们还可以使用jquery.Deferred的帮助。在这种情况下,asyncLoop函数将如下所示:
asyncLoop = function(array, callback) {
var nextElement, thisIteration;
if (array.length > 0) nextElement = array.pop();
thisIteration = callback(nextElement);
$.when(thisIteration).done(function(response) {
// here we can check value of response in order to break or whatever
if (array.length > 0) asyncLoop(array, collection, callback);
});
};
回调函数将如下所示:
addEntry = function(newEntry) {
var deferred, duplicateEntry;
// on the next line we can perform some check, which may cause async response.
duplicateEntry = someCheckHere();
if (duplicateEntry === true) {
deferred = $.Deferred();
// here we launch some other function (e.g. $.ajax or popup window)
// which based on result must call deferred.resolve([opt args - response])
// when deferred.resolve is called "asyncLoop" will start new iteration
// example function:
exampleFunction(duplicateEntry, deferred);
return deferred;
} else {
return someActionIfNotDuplicate();
}
};
解决延迟的示例函数:
function exampleFunction(entry, deffered){
openModal({
title: "what should we do with duplicate"
options: [
{name:"Replace", action: function(){replace(entry);deffered.resolve(replace:true)}},
{name: "Keep Existing", action: function(){deffered.resolve(replace:false)}}
]
})
}
我一直在使用“ setTimeout(Func,0);” 把戏大约一年。这是我最近写的一些研究报告,目的是说明如何加快速度。如果您只想要答案,请跳至步骤4。步骤1、2和3解释其原因和机制;否则,请转至步骤4。
// In Depth Analysis of the setTimeout(Func,0) trick.
//////// setTimeout(Func,0) Step 1 ////////////
// setTimeout and setInterval impose a minimum
// time limit of about 2 to 10 milliseconds.
console.log("start");
var workCounter=0;
var WorkHard = function()
{
if(workCounter>=2000) {console.log("done"); return;}
workCounter++;
setTimeout(WorkHard,0);
};
// this take about 9 seconds
// that works out to be about 4.5ms per iteration
// Now there is a subtle rule here that you can tweak
// This minimum is counted from the time the setTimeout was executed.
// THEREFORE:
console.log("start");
var workCounter=0;
var WorkHard = function()
{
if(workCounter>=2000) {console.log("done"); return;}
setTimeout(WorkHard,0);
workCounter++;
};
// This code is slightly faster because we register the setTimeout
// a line of code earlier. Actually, the speed difference is immesurable
// in this case, but the concept is true. Step 2 shows a measurable example.
///////////////////////////////////////////////
//////// setTimeout(Func,0) Step 2 ////////////
// Here is a measurable example of the concept covered in Step 1.
var StartWork = function()
{
console.log("start");
var startTime = new Date();
var workCounter=0;
var sum=0;
var WorkHard = function()
{
if(workCounter>=2000)
{
var ms = (new Date()).getTime() - startTime.getTime();
console.log("done: sum=" + sum + " time=" + ms + "ms");
return;
}
for(var i=0; i<1500000; i++) {sum++;}
workCounter++;
setTimeout(WorkHard,0);
};
WorkHard();
};
// This adds some difficulty to the work instead of just incrementing a number
// This prints "done: sum=3000000000 time=18809ms".
// So it took 18.8 seconds.
var StartWork = function()
{
console.log("start");
var startTime = new Date();
var workCounter=0;
var sum=0;
var WorkHard = function()
{
if(workCounter>=2000)
{
var ms = (new Date()).getTime() - startTime.getTime();
console.log("done: sum=" + sum + " time=" + ms + "ms");
return;
}
setTimeout(WorkHard,0);
for(var i=0; i<1500000; i++) {sum++;}
workCounter++;
};
WorkHard();
};
// Now, as we planned, we move the setTimeout to before the difficult part
// This prints: "done: sum=3000000000 time=12680ms"
// So it took 12.6 seconds. With a little math, (18.8-12.6)/2000 = 3.1ms
// We have effectively shaved off 3.1ms of the original 4.5ms of dead time.
// Assuming some of that time may be attributed to function calls and variable
// instantiations, we have eliminated the wait time imposed by setTimeout.
// LESSON LEARNED: If you want to use the setTimeout(Func,0) trick with high
// performance in mind, make sure your function takes more than 4.5ms, and set
// the next timeout at the start of your function, instead of the end.
///////////////////////////////////////////////
//////// setTimeout(Func,0) Step 3 ////////////
// The results of Step 2 are very educational, but it doesn't really tell us how to apply the
// concept to the real world. Step 2 says "make sure your function takes more than 4.5ms".
// No one makes functions that take 4.5ms. Functions either take a few microseconds,
// or several seconds, or several minutes. This magic 4.5ms is unattainable.
// To solve the problem, we introduce the concept of "Burn Time".
// Lets assume that you can break up your difficult function into pieces that take
// a few milliseconds or less to complete. Then the concept of Burn Time says,
// "crunch several of the individual pieces until we reach 4.5ms, then exit"
// Step 1 shows a function that is asyncronous, but takes 9 seconds to run. In reality
// we could have easilly incremented workCounter 2000 times in under a millisecond.
// So, duh, that should not be made asyncronous, its horrible. But what if you don't know
// how many times you need to increment the number, maybe you need to run the loop 20 times,
// maybe you need to run the loop 2 billion times.
console.log("start");
var startTime = new Date();
var workCounter=0;
for(var i=0; i<2000000000; i++) // 2 billion
{
workCounter++;
}
var ms = (new Date()).getTime() - startTime.getTime();
console.log("done: workCounter=" + workCounter + " time=" + ms + "ms");
// prints: "done: workCounter=2000000000 time=7214ms"
// So it took 7.2 seconds. Can we break this up into smaller pieces? Yes.
// I know, this is a retarded example, bear with me.
console.log("start");
var startTime = new Date();
var workCounter=0;
var each = function()
{
workCounter++;
};
for(var i=0; i<20000000; i++) // 20 million
{
each();
}
var ms = (new Date()).getTime() - startTime.getTime();
console.log("done: workCounter=" + workCounter + " time=" + ms + "ms");
// The easiest way is to break it up into 2 billion smaller pieces, each of which take
// only several picoseconds to run. Ok, actually, I am reducing the number from 2 billion
// to 20 million (100x less). Just adding a function call increases the complexity of the loop
// 100 fold. Good lesson for some other topic.
// prints: "done: workCounter=20000000 time=7648ms"
// So it took 7.6 seconds, thats a good starting point.
// Now, lets sprinkle in the async part with the burn concept
console.log("start");
var startTime = new Date();
var workCounter=0;
var index=0;
var end = 20000000;
var each = function()
{
workCounter++;
};
var Work = function()
{
var burnTimeout = new Date();
burnTimeout.setTime(burnTimeout.getTime() + 4.5); // burnTimeout set to 4.5ms in the future
while((new Date()) < burnTimeout)
{
if(index>=end)
{
var ms = (new Date()).getTime() - startTime.getTime();
console.log("done: workCounter=" + workCounter + " time=" + ms + "ms");
return;
}
each();
index++;
}
setTimeout(Work,0);
};
// prints "done: workCounter=20000000 time=107119ms"
// Sweet Jesus, I increased my 7.6 second function to 107.1 seconds.
// But it does prevent the browser from locking up, So i guess thats a plus.
// Again, the actual objective here is just to increment workCounter, so the overhead of all
// the async garbage is huge in comparison.
// Anyway, Lets start by taking advice from Step 2 and move the setTimeout above the hard part.
console.log("start");
var startTime = new Date();
var workCounter=0;
var index=0;
var end = 20000000;
var each = function()
{
workCounter++;
};
var Work = function()
{
if(index>=end) {return;}
setTimeout(Work,0);
var burnTimeout = new Date();
burnTimeout.setTime(burnTimeout.getTime() + 4.5); // burnTimeout set to 4.5ms in the future
while((new Date()) < burnTimeout)
{
if(index>=end)
{
var ms = (new Date()).getTime() - startTime.getTime();
console.log("done: workCounter=" + workCounter + " time=" + ms + "ms");
return;
}
each();
index++;
}
};
// This means we also have to check index right away because the last iteration will have nothing to do
// prints "done: workCounter=20000000 time=52892ms"
// So, it took 52.8 seconds. Improvement, but way slower than the native 7.6 seconds.
// The Burn Time is the number you tweak to get a nice balance between native loop speed
// and browser responsiveness. Lets change it from 4.5ms to 50ms, because we don't really need faster
// than 50ms gui response.
console.log("start");
var startTime = new Date();
var workCounter=0;
var index=0;
var end = 20000000;
var each = function()
{
workCounter++;
};
var Work = function()
{
if(index>=end) {return;}
setTimeout(Work,0);
var burnTimeout = new Date();
burnTimeout.setTime(burnTimeout.getTime() + 50); // burnTimeout set to 50ms in the future
while((new Date()) < burnTimeout)
{
if(index>=end)
{
var ms = (new Date()).getTime() - startTime.getTime();
console.log("done: workCounter=" + workCounter + " time=" + ms + "ms");
return;
}
each();
index++;
}
};
// prints "done: workCounter=20000000 time=52272ms"
// So it took 52.2 seconds. No real improvement here which proves that the imposed limits of setTimeout
// have been eliminated as long as the burn time is anything over 4.5ms
///////////////////////////////////////////////
//////// setTimeout(Func,0) Step 4 ////////////
// The performance numbers from Step 3 seem pretty grim, but GUI responsiveness is often worth it.
// Here is a short library that embodies these concepts and gives a descent interface.
var WilkesAsyncBurn = function()
{
var Now = function() {return (new Date());};
var CreateFutureDate = function(milliseconds)
{
var t = Now();
t.setTime(t.getTime() + milliseconds);
return t;
};
var For = function(start, end, eachCallback, finalCallback, msBurnTime)
{
var i = start;
var Each = function()
{
if(i==-1) {return;} //always does one last each with nothing to do
setTimeout(Each,0);
var burnTimeout = CreateFutureDate(msBurnTime);
while(Now() < burnTimeout)
{
if(i>=end) {i=-1; finalCallback(); return;}
eachCallback(i);
i++;
}
};
Each();
};
var ForEach = function(array, eachCallback, finalCallback, msBurnTime)
{
var i = 0;
var len = array.length;
var Each = function()
{
if(i==-1) {return;}
setTimeout(Each,0);
var burnTimeout = CreateFutureDate(msBurnTime);
while(Now() < burnTimeout)
{
if(i>=len) {i=-1; finalCallback(array); return;}
eachCallback(i, array[i]);
i++;
}
};
Each();
};
var pub = {};
pub.For = For; //eachCallback(index); finalCallback();
pub.ForEach = ForEach; //eachCallback(index,value); finalCallback(array);
WilkesAsyncBurn = pub;
};
///////////////////////////////////////////////
//////// setTimeout(Func,0) Step 5 ////////////
// Here is an examples of how to use the library from Step 4.
WilkesAsyncBurn(); // Init the library
console.log("start");
var startTime = new Date();
var workCounter=0;
var FuncEach = function()
{
if(workCounter%1000==0)
{
var s = "<div></div>";
var div = jQuery("*[class~=r1]");
div.append(s);
}
workCounter++;
};
var FuncFinal = function()
{
var ms = (new Date()).getTime() - startTime.getTime();
console.log("done: workCounter=" + workCounter + " time=" + ms + "ms");
};
WilkesAsyncBurn.For(0,2000000,FuncEach,FuncFinal,50);
// prints: "done: workCounter=20000000 time=149303ms"
// Also appends a few thousand divs to the html page, about 20 at a time.
// The browser is responsive the entire time, mission accomplished
// LESSON LEARNED: If your code pieces are super tiny, like incrementing a number, or walking through
// an array summing the numbers, then just putting it in an "each" function is going to kill you.
// You can still use the concept here, but your "each" function should also have a for loop in it
// where you burn a few hundred items manually.
///////////////////////////////////////////////
给定一个异步工作器函数someFunction,该函数将使用一个result参数说明结果循环是否应继续进行调用,以返回结果函数:
// having:
// function someFunction(param1, praram2, resultfunc))
// function done() { alert("For cycle ended"); }
(function(f){ f(f) })(function(f){
someFunction("param1", "praram2", function(result){
if (result)
f(f); // loop continues
else
done(); // loop ends
});
})
为了检查是否结束循环,辅助函数someFunction可以将结果函数转发到其他异步操作。另外,可以通过将函数done用作回调将整个表达式封装到异步函数中。
如果您喜欢wilsonpage的答案,但更习惯使用async.js的语法,则可以使用以下变体:
function asyncEach(iterableList, callback, done) {
var i = -1,
length = iterableList.length;
function loop() {
i++;
if (i === length) {
done();
return;
}
callback(iterableList[i], loop);
}
loop();
}
asyncEach(['A', 'B', 'C'], function(item, callback) {
setTimeout(function(){
document.write('Iteration ' + item + ' <br>');
callback();
}, 1000);
}, function() {
document.write('All done!');
});
演示可以在这里找到-http: //jsfiddle.net/NXTv7/8/
我认为这是另一个比其他示例更具可读性的示例,其中将异步函数包装在一个done函数中,该函数接收一个函数,当前循环索引以及上一个异步调用的结果(如果有):
function (done, i, prevResult) {
// perform async stuff
// call "done(result)" in async callback
// or after promise resolves
}
一旦done()被调用,它将触发下一个异步调用,再次传入完成函数,当前索引和上一个结果。整个循环完成后,callback将调用提供的循环。
您可以运行以下代码段:
asyncLoop({
limit: 25,
asyncLoopFunction: function(done, i, prevResult) {
setTimeout(function() {
console.log("Starting Iteration: ", i);
console.log("Previous Result: ", prevResult);
var result = i * 100;
done(result);
}, 1000);
},
initialArgs: 'Hello',
callback: function(result) {
console.log('All Done. Final result: ', result);
}
});
function asyncLoop(obj) {
var limit = obj.limit,
asyncLoopFunction = obj.asyncLoopFunction,
initialArgs = obj.initialArgs || {},
callback = obj.callback,
i = 0;
function done(result) {
i++;
if (i < limit) {
triggerAsync(result);
} else {
callback(result);
}
}
function triggerAsync(prevResult) {
asyncLoopFunction(done, i, prevResult);
}
triggerAsync(initialArgs); // init
}
您可以使用async awaitES7中引入的:
for ( /* ... */ ) {
let result = await someFunction(param1, param2);
}
alert("For cycle ended");
仅在someFunction返回承诺时有效!
如果someFunction未返回承诺,那么您可以这样使它自己返回承诺:
function asyncSomeFunction(param1,praram2) {
return new Promise((resolve, reject) => {
someFunction(praram1,praram2,(result)=>{
resolve(result);
})
})
}
然后将其替换await someFunction(param1, param2);为await asynSomeFunction(param1, param2);
请在编写async await代码之前了解Promises !
Unexpected await inside loop。
javascript问题。该警告来自您的eslint配置。我总是禁止使用该规则,eslint因为在大多数地方,我确实需要在循环中等待
http://cuzztuts.blogspot.ro/2011/12/js-async-for-very-cool.html
编辑:
来自github的链接:https : //github.com/cuzzea/lib_repo/blob/master/cuzzea/js/functions/core/async_for.js
function async_for_each(object,settings){
var l=object.length;
settings.limit = settings.limit || Math.round(l/100);
settings.start = settings.start || 0;
settings.timeout = settings.timeout || 1;
for(var i=settings.start;i<l;i++){
if(i-settings.start>=settings.limit){
setTimeout(function(){
settings.start = i;
async_for_each(object,settings)
},settings.timeout);
settings.limit_callback ? settings.limit_callback(i,l) : null;
return false;
}else{
settings.cbk ? settings.cbk(i,object[i]) : null;
}
}
settings.end_cbk?settings.end_cbk():null;
return true;
}
此功能允许您使用settings.limit在for循环中创建百分比中断。limit属性只是一个整数,但是当设置为array.length * 0.1时,这将使settings.limit_callback每10%被调用一次。
/*
* params:
* object: the array to parse
* settings_object:
* cbk: function to call whenwhen object is found in array
* params: i,object[i]
* limit_calback: function to call when limit is reached
* params: i, object_length
* end_cbk: function to call when loop is finished
* params: none
* limit: number of iteration before breacking the for loop
* default: object.length/100
* timeout: time until start of the for loop(ms)
* default: 1
* start: the index from where to start the for loop
* default: 0
*/
例子:
var a = [];
a.length = 1000;
async_for_each(a,{
limit_callback:function(i,l){console.log("loading %s/%s - %s%",i,l,Math.round(i*100/l))}
});
基于承诺库的解决方案:
/*
Since this is an open question for JS I have used Kris Kowal's Q promises for the same
*/
var Q = require('q');
/*
Your LOOP body
@success is a parameter(s) you might pass
*/
var loopBody = function(success) {
var d = Q.defer(); /* OR use your favorite promise library like $q in angular */
/*
'setTimeout' will ideally be your node-like callback with this signature ... (err, data) {}
as shown, on success you should resolve
on failure you should reject (as always ...)
*/
setTimeout(function(err, data) {
if (!err) {
d.resolve('success');
} else {
d.reject('failure');
}
}, 100); //100 ms used for illustration only
return d.promise;
};
/*
function to call your loop body
*/
function loop(itr, fn) {
var def = Q.defer();
if (itr <= 0) {
def.reject({ status: "un-successful " });
} else {
var next = loop.bind(undefined, itr - 1, fn); // 'next' is all there is to this
var callback = fn.bind(undefined /*, a, b, c.... */ ); // in case you want to pass some parameters into your loop body
def.promise = callback().then(def.resolve, next);
}
return def.promise;
}
/*
USAGE: loop(iterations, function(){})
the second argument has to be thenable (in other words return a promise)
NOTE: this loop will stop when loop body resolves to a success
Example: Try to upload file 3 times. HURRAY (if successful) or log failed
*/
loop(4, loopBody).then(function() {
//success handler
console.log('HURRAY')
}, function() {
//failed
console.log('failed');
});
我需要调用一些异步函数X时间,每次迭代都必须在上一个迭代完成之后发生,所以我编写了一个litte库,可以这样使用:
// https://codepen.io/anon/pen/MOvxaX?editors=0012
var loop = AsyncLoop(function(iteration, value){
console.log("Loop called with iteration and value set to: ", iteration, value);
var random = Math.random()*500;
if(random < 200)
return false;
return new Promise(function(resolve){
setTimeout(resolve.bind(null, random), random);
});
})
.finished(function(){
console.log("Loop has ended");
});
每次调用用户定义的循环函数时,它都有两个参数,即迭代索引和上一个调用返回值。
这是输出示例:
"Loop called with iteration and value set to: " 0 null
"Loop called with iteration and value set to: " 1 496.4137048207333
"Loop called with iteration and value set to: " 2 259.6020382449663
"Loop called with iteration and value set to: " 3 485.5400568702862
"Loop has ended"
( /* ... */ )看起来像个怪物,我现在很害怕:(