2012-06-13 3 views
4

Я надеюсь, что это будет иметь смысл: мне нужно создать функцию Еогеасп в JavaScript, который будет использоваться как это:Асинхронный Еогеасп в JavaScript

foreach(["A", "B", "C"], function(letter, done) { 
    // do something async with 'letter' 
    doSomthing(letter, done); // *** 
}, function() { 
    // final callback that is called after all array has been visted. 
    // do some final work 
}); 

Так я думал о следующей реализации:

var foreach = function(array, func, ready) { 
    if (!array.length) 
     ready(); 
    var that = this; 
    func(array[0], function(){ 
     that.foreach(array.slice(1, array.length), func, ready); 
    }); 
} 

И кажется, что он действительно работает! очень круто.

Но я думал, есть ли решение, которое не использует рекурсию? Я не мог придумать ...

+0

ли первая функция (второй параметр для 'foreach') должна работать на каждом элементе массива асинхронно? –

+2

В этом коде нет ничего асинхронного. Что вы пытаетесь передать? –

+1

Theres что-то асинхронное о названии вопроса ... –

ответ

1

Ваш подход технически правильный, но это нехорошо делать так. Pls реализовать с использованием шаблона обещания в javasript. Я рекомендую вам с помощью when.js с открытым исходным кодом, JS на мерзавца для реализации обещание шаблона Pls refert в коде ниже

var service = { 
      fetch: function (query) { 

       // return a promise from the function 
       return when(["A", "B", "C"].forEach(function (name) { 
        alert(name); 
       })); 
      } 
     }; 

     service.fetch("hello world").then(function() { 
      alert("work has been completed"); 
     }); 
+0

Спасибо! это тот ответ, на который я надеялся. Что вы подразумеваете под словом «нехорошо делать» из-за предварительной работы? – Ofri

+0

производительность мудрый я не вижу никакой проблемы. Я изменил код с примером, который может помочь –

+0

Это должно использоваться с асинхронными операциями вообще, но синтаксис остается тем же самым –

2

Проверьте библиотеку async. Он имеет несколько разных функций, и он активно поддерживается.

+0

Спасибо! Хотя меня все еще интересует реализация ... Думаю, я мог бы посмотреть на библиотечный код :) – Ofri

0

Поправьте меня, если я ошибаюсь, но от того, что я понял из вашего вопроса я верю, что вы хотите чтобы взять массив, последовательно вызвать функцию асинхронно на каждом члене этого массива, а затем выполнить функцию обратного вызова, когда каждый элемент массива обработан.

Теперь, чтобы выполнить функцию асинхронно в среде браузера мы будем делать что-то вроде this:

Function.prototype.async = function() { 
    setTimeout.bind(window, this, 0).apply(window, arguments); 
}; 

alert.async(5); 
alert(6); 

В приведенном выше примере функция setTimeout используется для вызова данной функции асинхронно, благодаря которой мы впервые видим значение 6, а затем предупреждение 5.

Далее, чтобы сделать вашу foreach функцию асинхронно мы будем делать что-то, как follows:

function forEach(array, funct, callback) { 
    if (array.length) 
    funct.async(array[0], forEach.bind(null, array.slice(1), funct, callback)); 
    else callback.async(); 
} 

выше решение не использовать recusrion. Уверен, что функция forEach ссылается внутри себя, но она вызывается только в функции funct, которая называется асинхронно. Следовательно, функция forEach возвращается до того, как она снова вызвана в функции funct.

Я включил ссылки на JS fiddles перед каждым фрагментом кода. Если у вас есть еще какие-то сомнения, я был бы рад ответить на них.

Edit:

Если вам не нравится, изменяющие prototype из Function (@ kitgui.com), то вы можете использовать этот modified code:

var async = Function.prototype.call.bind(function() { 
    setTimeout.bind(null, this, 0).apply(null, arguments); 
}); 

async(alert, 5); 
alert(6); 

Поскольку я не ссылается window в над кодом он также будет работать в не-браузерных средах.

Тогда мы можем переписать функцию forEach следующим образом:

function forEach(array, funct, callback) { 
    if (array.length) 
    async(funct, array[0], forEach.bind(null, array.slice(1), funct, callback)); 
    else async(callback); 
} 

Там у нас есть это. Не нужно изменять prototype от Function. Тело функции async в значительной степени то же самое. Мы просто создали unbound wrapper для этого, используя call.bind. Вы можете увидеть live demo для себя.

Бонус:

Вы можете создать errbacks используя вышеупомянутую картину следующим образом (см live demo):

function forEach(array, funct, callback, error) { 
    if (array.length && !error) 
    async(funct, array[0], forEach.bind(null, array.slice(1), funct, callback)); 
    else async(callback, error || null); 
} 

Это эквивалентно forEachSeries функции в async библиотеке caolan в возрасте до 10 строк кода.

+1

На самом деле, ваш код настолько хорош, что мой выглядит довольно плохо, хотя его действительно стандартная ярмарка и выполняет свою работу. -1 был немного суровым. Я просто посмотрел на твои вещи и на человека, вот некоторые интересные идеи. он так много делает в таком маленьком пространстве. Я искал некоторые вещи, которые вы делали, и очень впечатляюще. супер элегантный. делая bind() и уменьшая массив, очень круто. это Джон Ресиг, добрый человек. но вы по-прежнему немного настроены в отношении, но очень удивительный код наверняка. –

+0

Спасибо. Я ценю тот факт, что вам потребовалось некоторое время, чтобы изучить мой код и понять, что я сделал. Я согласен с тем, что я был слишком критичен, когда приходил к суду вашего ответа, и за это я извиняюсь. Тем не менее, спасибо за указание на мои недостатки. Я буду работать над ними. Приветствия. =) –

+0

спасибо за обновление. Вы показали мне очень хорошие идеи. Я написал немного кода, чтобы продемонстрировать свою конкретную потребность и как я это сделал. http://www.emeraldcode.com/sleep-test.htm. Он демонстрирует, что AJAX возвращается в разное время, что на самом деле является для меня целью использования счетчика. Если у вас есть предложения в этом сценарии, которые действительно нужны мне, это было бы очень полезно. –

1

Я просто здесь снова для академических целей на этом этапе. Теперь я рекомендую всем понять подход Аадита в его изяществе и терпении в первую очередь, потому что это был хороший опыт для меня в его методе использования bind, поскольку я не знал об этом и не знал о дополнительных аргументах, которые вы можете разместить после setTimeout ,

После изучения этих вещей, Я уменьшил свой код к этому:

var foreach = function(array,doSomething,onComplete) { 
     var i = 0, len = array.length, completeCount = 0; 
     for(;i < len; i++) { 
      window.setTimeout(function() { 
       doSomething(arguments[0]); 
       completeCount++; 
       if (completeCount === len) { 
        onComplete(); 
       } 
      },0,array[i]); 
     } 
    }; 

Я считаю, что вы должны иметь «completeCount», потому что, хотя код Aadit является отличным лаконичен рабочим раствором, который уменьшая массив автоматически, он не является по-настоящему асинхронным, поскольку «next()» вызывается после того, как каждый метод закончен в массиве линейно. «CompleteCount» позволяет коду завершить выполнение в любом порядке, в котором я считаю. В коде Aadit существует также побочный эффект изменения вашего входного массива, а также необходимость изменения прототипа Function, в котором я утверждаю, что это необязательно. «Подъем» также не практикуется в его коде, который, как мне кажется, должен быть выполнен, поскольку этот стиль уменьшает ошибки.

Снова я очень уважаю код Aadit и нашел время вернуться, чтобы попытаться представить лучшее решение, основанное на том, что я узнал через других умных людей, а также Aadit. Я приветствую любые критические замечания и исправления, которые я попытаюсь извлечь из этого.

FYI: вот один, который является общим отсроченным методом

var deferred = function(methods,onComplete) { 
     var i = 0, len = methods.length, completeCount = 0, 
     partialComplete = function() { 
      completeCount++; 
      if (completeCount === len) { 
       onComplete(); 
      } 
     }; 
     for(;i < len; i++) { 
      window.setTimeout(function() { 
       arguments[0](partialComplete); 
      },0,methods[i]); 
     } 
    }; 

    // how to call it 
    deferred([ 
     function (complete) { 
      // this could easily be ajax that calls "complete" when ready 
      complete(); 
     }, 
     function (complete) { 
      complete();  
     } 
    ], function() { 
     alert('done'); 
    }); 
+1

Я хотел бы указать на несколько заблуждений. Во-первых, вызов функции 'next' для обработки следующего элемента массива не делает функцию' forEach' синхронной.Это связано с тем, что метод 'forEach' возвращается до того, как вызывается функция' funct', поскольку она вызывает асинхронную функцию 'funct'. Вызов 'next' также обрабатывает следующий элемент массива асинхронно. Следовательно, моя реализация 'forEach' действительно действительно асинхронна. –

+1

Во-вторых, сохранение 'completeCount' не означает, что элементы массива будут обработаны не в порядке. Когда вы вызываете 'setTimeout', механизм JavaScript помещает код в очередь. Следовательно, хотя вы вызываете каждую функцию асинхронно, они всегда обрабатываются по порядку. Вам не нужно об этом говорить. Смотрите [выход] (http://jsfiddle.net/tzpE9/) для себя. Преимущество использования функции 'next' заключается в том, что вы можете прекратить обработку массива в любое время, просто не называя' next'. Использование счетчика - это просто дополнительные накладные расходы. –

+1

В-третьих, моя реализация 'forEach' не имеет побочных эффектов. Входной массив никогда не изменяется. Вызов метода 'slice' в массиве не мутирует массив. Он просто возвращает новый массив. Вы можете проверить это самостоятельно, изучив следующий [сценарий] (http://jsfiddle.net/dGLvD/1/). Обратите внимание, что когда вызывается функция 'done', исходный массив все еще остается нетронутым. –

0

Предполагая, что вы хотите выполнить необработанное вычисление, и хотите, чтобы он был асинхронным, поэтому он не блокирует браузер.

Я использовал «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. 
/////////////////////////////////////////////// 
Смежные вопросы