4

我希望这是有道理的:我需要在 javascript 中创建一个 foreach 函数,它将像这样使用:

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

而且它似乎确实有效!很酷。

但我在想是否有不使用递归的解决方案?我想不出一个...

4

6 回答 6

1

在这一点上,我只是出于学术目的再次来到这里。现在,我建议大家首先了解 Aadit 的优雅和简洁的方法,因为这对我来说是一次很好的学习体验,因为我不知道这一点,也不知道你可以在 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 那里学到的知识提出更好的解决方案。我欢迎任何批评和更正,因为我会努力从中学习。

仅供参考:这是一种通用的延迟方法

    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'); 
    });
于 2012-06-19T12:40:27.767 回答
1

您的方法在技术上是正确的,但这样做并不好。请在 javasript 中使用 promise 模式实现。我建议您使用 git 上可用的开源 js when.js 来实现承诺模式请参考以下代码

   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");
        });
于 2012-06-13T05:02:53.553 回答
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.  
///////////////////////////////////////////////
于 2013-03-29T20:22:52.927 回答
0

查看异步库。它有几个不同的功能,并且得到积极的支持。

于 2012-06-13T04:49:20.873 回答
0

如果我错了,请纠正我,但从我对您的问题的理解来看,我相信您想要获取一个数组,在该数组的每个成员上异步串行调用一个函数,然后在处理完数组的每个成员后执行一个回调函数.

现在要在浏览器环境中异步执行函数,我们将执行以下操作:

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

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

在上面的示例中,该setTimeout函数用于异步调用给定函数,因此我们首先看到该值6,然后5是警报值。

接下来,为了使您的foreach函数异步,我们将执行以下操作:

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

上述解决方案不使用recursion。当然该forEach函数在其内部被引用,但它仅在funct异步调用的函数中被调用。因此,forEach函数在函数中再次调用之前返回funct

我在每个代码片段之前都包含了指向 JS fiddles 的链接。如果您还有任何疑问,我很乐意为您解答。

编辑:

如果您不喜欢修改prototype( Function@kitgui.com),那么您可以使用此修改后的代码

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几乎相同。我们简单使用call.bind. 您可以亲自观看现场演示

奖金:

您可以使用上述模式创建 errbacks,如下所示(参见现场演示):

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

这相当于forEachSeriescaolan的async库中的函数,不到10行代码。

于 2012-06-13T05:56:42.630 回答
-1

我发现本教程准确地解释了我需要什么。我希望这也会对其他人有所帮助。 http://nodetuts.com/tutorials/19-asynchronous-iteration-patterns.html#video

于 2012-07-26T11:26:42.603 回答