在JavaScript中循环的异步

在继续之前,我需要一个等待异步调用的循环。 就像是:

for ( /* ... */ ) { someFunction(param1, praram2, function(result) { // Okay, for cycle could continue }) } alert("For cycle ended"); 

我怎么能这样做? 你有什么想法吗?

如果阻止脚本,则不能在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 

我简化了这个:

功能:

 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!'); } }); 

示例: http : //jsfiddle.net/NXTv7/8/

@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的异步方法队列。

也看这个灿烂的图书馆caolan / async 。 您可以使用mapSeries或系列轻松完成for循环。

如果你的例子中有更多的细节,我可以发布一些示例代码。

我们也可以使用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 (eg $.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解释推理和机制;

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

为了检查是否结束循环,worker函数someFunction可以将结果函数转发给其他异步操作。 而且,整个表达式可以通过把一个函数作为回调来封装成一个异步函数。

如果你喜欢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 } 

您可以使用ES7中引入的async await

 for ( /* ... */ ) { let result = await someFunction(param1, param2); } alert("For cycle ended"); 

这只适用于someFunction正在返回一个承诺!

如果someFunction函数没有返回一个Promise,那么你可以让它自己像这样返回一个Promise:

 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代码之前了解Promise!

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