การโทรกลับหลังจากการโทรกลับแบบอะซิงโครนัสทั้งหมดเสร็จสมบูรณ์


245

ตามที่ชื่อแนะนำ ฉันจะทำสิ่งนี้ได้อย่างไร

ฉันต้องการโทรwhenAllDone()หลังจาก forEach-loop ผ่านแต่ละองค์ประกอบแล้วทำการประมวลผลแบบอะซิงโครนัส

[1, 2, 3].forEach(
  function(item, index, array, done) {
     asyncFunction(item, function itemDone() {
       console.log(item + " done");
       done();
     });
  }, function allDone() {
     console.log("All done");
     whenAllDone();
  }
);

เป็นไปได้ไหมที่จะทำงานเช่นนี้? เมื่ออาร์กิวเมนต์ตัวที่สองสำหรับ forEach คือฟังก์ชันการเรียกกลับที่ทำงานเมื่อมันผ่านการวนซ้ำทั้งหมด?

ผลลัพธ์ที่คาดหวัง:

3 done
1 done
2 done
All done!

13
มันจะดีถ้าforEachวิธีอาร์เรย์มาตรฐานมีdoneพารามิเตอร์allDoneโทรกลับและโทรกลับ!
Vanuan

22
มันเป็นความอัปยศจริง ๆ ดังนั้นง่าย ๆ ต้องมีการต่อสู้อย่างมากใน JavaScript
Ali

คำตอบ:


410

Array.forEach ไม่ได้ให้สิ่งที่ดี (ถ้ามี) แต่มีหลายวิธีในการบรรลุสิ่งที่คุณต้องการ:

ใช้เคาน์เตอร์ง่าย ๆ

function callback () { console.log('all done'); }

var itemsProcessed = 0;

[1, 2, 3].forEach((item, index, array) => {
  asyncFunction(item, () => {
    itemsProcessed++;
    if(itemsProcessed === array.length) {
      callback();
    }
  });
});

(ขอบคุณ @vanuan และคนอื่น ๆ ) วิธีการนี้รับประกันว่ารายการทั้งหมดจะได้รับการประมวลผลก่อนที่จะเรียกกลับ "เสร็จสิ้น" คุณต้องใช้ตัวนับที่ได้รับการอัพเดตในการติดต่อกลับ ทั้งนี้ขึ้นอยู่กับค่าของพารามิเตอร์ดัชนีไม่ได้ให้การรับประกันเดียวกันเนื่องจากลำดับของการส่งคืนของการดำเนินการแบบอะซิงโครนัสไม่ได้รับประกัน

ใช้สัญญา ES6

(ห้องสมุดสัญญาสามารถใช้กับเบราว์เซอร์รุ่นเก่า):

  1. ประมวลผลคำขอทั้งหมดที่รับประกันการดำเนินการแบบซิงโครนัส (เช่น 1 จาก 2 และ 3)

    function asyncFunction (item, cb) {
      setTimeout(() => {
        console.log('done with', item);
        cb();
      }, 100);
    }
    
    let requests = [1, 2, 3].reduce((promiseChain, item) => {
        return promiseChain.then(() => new Promise((resolve) => {
          asyncFunction(item, resolve);
        }));
    }, Promise.resolve());
    
    requests.then(() => console.log('done'))
  2. ประมวลผลคำขอ async ทั้งหมดโดยไม่ต้องดำเนินการ "ซิงโครนัส" (2 อาจเสร็จเร็วกว่า 1)

    let requests = [1,2,3].map((item) => {
        return new Promise((resolve) => {
          asyncFunction(item, resolve);
        });
    })
    
    Promise.all(requests).then(() => console.log('done'));

ใช้ไลบรารี async

มีไลบรารีแบบอะซิงโครนัสอื่น ๆ asyncเป็นที่นิยมมากที่สุดที่ให้กลไกที่จะแสดงสิ่งที่คุณต้องการ

แก้ไข

เนื้อความของคำถามได้รับการแก้ไขเพื่อลบรหัสตัวอย่างก่อนหน้านี้ซิงโครนัสดังนั้นฉันจึงได้อัปเดตคำตอบของฉันเพื่อชี้แจง ตัวอย่างดั้งเดิมที่ใช้ซิงโครนัสเช่นรหัสเพื่อจำลองพฤติกรรมแบบอะซิงโครนัสดังนั้นจึงมีการนำไปใช้ดังต่อไปนี้:

array.forEachเป็นแบบซิงโครนัสและเป็นres.writeเช่นนั้นดังนั้นคุณสามารถเพียงแค่โทรกลับหลังจากที่คุณโทรไปที่หน้า:

  posts.foreach(function(v, i) {
    res.write(v + ". index " + i);
  });

  res.end();

31
อย่างไรก็ตามโปรดทราบว่าหากมีสิ่งที่ไม่ตรงกันภายใน forEach (เช่นคุณวนลูปผ่านอาร์เรย์ของ URL และทำ HTTP GET บนสิ่งเหล่านั้น) ไม่มีการรับประกันว่า res.end จะถูกเรียกใช้ล่าสุด
AlexMA

ในการเริ่มการโทรกลับหลังจากดำเนินการ async ในลูปคุณสามารถใช้ยูทิลิตี async ของแต่ละวิธี: github.com/caolan/async#each
elkelk

2
@Vanuan ผมได้ปรับปรุงคำตอบของฉันให้ดีขึ้นตรงกับการแก้ไขอย่างมีนัยสำคัญค่อนข้าง :)
นิค Tomlin

4
ทำไมต้องไม่ใช่if(index === array.length - 1)และลบitemsProcessed
Amin Jafari

5
@AminJafari เนื่องจากการโทรแบบอะซิงโครนัสอาจไม่สามารถแก้ไขได้ตามลำดับการลงทะเบียนที่แน่นอน (บอกว่าคุณกำลังโทรออกไปยังเซิร์ฟเวอร์ การเรียกแบบอะซิงโครนัสครั้งล่าสุดสามารถแก้ไขได้ก่อนการโทรครั้งก่อน การปิดบังยามตอบโต้เรื่องนี้นับตั้งแต่การเรียกกลับทั้งหมดต้องดำเนินการโดยไม่คำนึงถึงลำดับที่พวกเขาแก้ไข
Nick Tomlin

25

หากคุณพบฟังก์ชั่นแบบอะซิงโครนัสและคุณต้องการตรวจสอบให้แน่ใจว่าก่อนที่จะเรียกใช้รหัสมันทำงานให้เสร็จสิ้นเราสามารถใช้ความสามารถในการโทรกลับได้เสมอ

ตัวอย่างเช่น:

var ctr = 0;
posts.forEach(function(element, index, array){
    asynchronous(function(data){
         ctr++; 
         if (ctr === array.length) {
             functionAfterForEach();
         }
    })
});

หมายเหตุ: functionAfterForEachเป็นฟังก์ชั่นที่จะทำงานหลังจากเสร็จสิ้นงานก่อนหน้า asynchronousเป็นฟังก์ชั่นแบบอะซิงโครนัสที่ดำเนินการภายใน foreach


9
สิ่งนี้จะไม่ทำงานเนื่องจากลำดับของการดำเนินการตามคำขอแบบอะซิงโครนัสไม่ใช่แบบ Warenteed การร้องขอ async ครั้งสุดท้ายอาจจบลงก่อนที่คนอื่น ๆ และเรียกใช้งาน functionAfterForEach () ก่อนที่จะทำการร้องขอทั้งหมด
Rémy DAVID

@ RémyDAVIDคุณมีจุดที่เกี่ยวข้องกับคำสั่งของการดำเนินการหรือฉันจะบอกว่ากระบวนการเสร็จสิ้นนานแค่ไหน แต่จาวาสคริปต์เป็นเธรดเดี่ยวดังนั้นจึงใช้งานได้ในที่สุด และหลักฐานคือ upvote คำตอบนี้ได้รับ
Emil Reña Enriquez

1
ฉันไม่แน่ใจเหมือนกันว่าทำไมคุณมี upvotes มากมาย แต่Rémiนั้นถูกต้อง รหัสของคุณจะไม่ทำงานเลยเนื่องจากอะซิงโครนัสหมายถึงคำขอใด ๆ อาจส่งคืนได้ตลอดเวลา แม้ว่า JavaScript จะไม่ใช่มัลติเธรด แต่เบราว์เซอร์ของคุณคือ อย่างหนักฉันอาจเพิ่ม มันอาจจะเรียกคนใดคนหนึ่งของคุณโทรกลับได้ตลอดเวลาในลำดับใด ๆ ขึ้นอยู่กับเมื่อได้รับการตอบกลับจากเซิร์ฟเวอร์ ...
อเล็กซิส Wilke

2
ใช่นี่คือคำตอบที่ผิดอย่างสมบูรณ์ หากฉันรันการดาวน์โหลด 10 ครั้งในแบบขนานมันเป็นเพียงการรับประกันว่าการดาวน์โหลดครั้งสุดท้ายจะเสร็จสิ้นก่อนเวลาที่เหลือและยุติการดำเนินการ
knrdk

ฉันขอแนะนำให้คุณใช้ตัวนับเพื่อเพิ่มจำนวนงานอะซิงโครนัสที่เสร็จสมบูรณ์แล้วและจับคู่กับความยาวของอาร์เรย์แทนที่จะเป็นดัชนี จำนวนการ upvotes ไม่เกี่ยวข้องกับการพิสูจน์ความถูกต้องของคำตอบ
Alex

17

หวังว่านี่จะช่วยแก้ปัญหาของคุณได้ฉันมักจะทำงานกับสิ่งนี้เมื่อฉันต้องดำเนินการ forEach กับงานอะซิงโครนัสภายใน

foo = [a,b,c,d];
waiting = foo.length;
foo.forEach(function(entry){
      doAsynchronousFunction(entry,finish) //call finish after each entry
}
function finish(){
      waiting--;
      if (waiting==0) {
          //do your Job intended to be done after forEach is completed
      } 
}

กับ

function doAsynchronousFunction(entry,callback){
       //asynchronousjob with entry
       callback();
}

ฉันมีปัญหาที่คล้ายกันในรหัส Angular 9 ของฉันและคำตอบนี้ได้หลอกลวงสำหรับฉัน แม้ว่าคำตอบ @Emil Reña Enriquez ก็ใช้ได้สำหรับฉันเช่นกัน แต่ฉันคิดว่านี่เป็นคำตอบที่ถูกต้องและง่ายขึ้นสำหรับปัญหานี้
omostan

17

มันแปลกมากที่มีคำตอบที่ไม่ถูกต้องให้กับกรณีอะซิงโครนัส ! มันสามารถแสดงให้เห็นว่าดัชนีการตรวจสอบไม่ได้มีพฤติกรรมที่คาดหวัง:

// INCORRECT
var list = [4000, 2000];
list.forEach(function(l, index) {
    console.log(l + ' started ...');
    setTimeout(function() {
        console.log(index + ': ' + l);
    }, l);
});

เอาท์พุท:

4000 started
2000 started
1: 2000
0: 4000

หากเราตรวจสอบการindex === array.length - 1โทรกลับจะถูกเรียกเมื่อเสร็จสิ้นการทำซ้ำครั้งแรกในขณะที่องค์ประกอบแรกยังคงค้างอยู่!

เพื่อแก้ปัญหานี้โดยไม่ต้องใช้ไลบรารีภายนอกเช่น async ฉันคิดว่าทางออกที่ดีที่สุดของคุณคือการบันทึกความยาวของรายการและลดลงหากหลังจากการทำซ้ำแต่ละครั้ง เนื่องจากมีเพียงหนึ่งเธรดเราจึงมั่นใจว่าไม่มีโอกาสเกิดสภาวะการแข่งขัน

var list = [4000, 2000];
var counter = list.length;
list.forEach(function(l, index) {
    console.log(l + ' started ...');
    setTimeout(function() {
        console.log(index + ': ' + l);
        counter -= 1;
        if ( counter === 0)
            // call your callback here
    }, l);
});

1
นั่นอาจเป็นทางออกเดียว ไลบรารี async ใช้ตัวนับด้วยหรือไม่
Vanuan

1
แม้ว่าวิธีการแก้ปัญหาอื่นจะทำงานได้ แต่สิ่งนี้น่าสนใจที่สุดเพราะไม่จำเป็นต้องผูกมัดหรือเพิ่มความซับซ้อน KISS
azatar

โปรดพิจารณาสถานการณ์เมื่อความยาวของอาร์เรย์เป็นศูนย์ในกรณีนี้การโทรกลับจะไม่ถูกเรียก
Saeed Ir

6

ด้วย ES2018 คุณสามารถใช้ตัวทำซ้ำ async:

const asyncFunction = a => fetch(a);
const itemDone = a => console.log(a);

async function example() {
  const arrayOfFetchPromises = [1, 2, 3].map(asyncFunction);

  for await (const item of arrayOfFetchPromises) {
    itemDone(item);
  }

  console.log('All done');
}

1
Availabe in Node v10
Matt Swezey

2

โซลูชันของฉันที่ไม่มีสัญญา (สิ่งนี้ทำให้มั่นใจได้ว่าการกระทำทุกอย่างจะสิ้นสุดลงก่อนที่การดำเนินการต่อไปจะเริ่มต้น):

Array.prototype.forEachAsync = function (callback, end) {
        var self = this;
    
        function task(index) {
            var x = self[index];
            if (index >= self.length) {
                end()
            }
            else {
                callback(self[index], index, self, function () {
                    task(index + 1);
                });
            }
        }
    
        task(0);
    };
    
    
    var i = 0;
    var myArray = Array.apply(null, Array(10)).map(function(item) { return i++; });
    console.log(JSON.stringify(myArray));
    myArray.forEachAsync(function(item, index, arr, next){
      setTimeout(function(){
        $(".toto").append("<div>item index " + item + " done</div>");
        console.log("action " + item + " done");
        next();
      }, 300);
    }, function(){
        $(".toto").append("<div>ALL ACTIONS ARE DONE</div>");
        console.log("ALL ACTIONS ARE DONE");
    });
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div class="toto">

</div>


1
 var counter = 0;
 var listArray = [0, 1, 2, 3, 4];
 function callBack() {
     if (listArray.length === counter) {
         console.log('All Done')
     }
 };
 listArray.forEach(function(element){
     console.log(element);
     counter = counter + 1;
     callBack();
 });

1
มันจะไม่ทำงานเพราะถ้าคุณจะมีการดำเนินการ async ภายใน foreach
Sudhanshu Gaur


0

ทางออกของฉัน:

//Object forEachDone

Object.defineProperty(Array.prototype, "forEachDone", {
    enumerable: false,
    value: function(task, cb){
        var counter = 0;
        this.forEach(function(item, index, array){
            task(item, index, array);
            if(array.length === ++counter){
                if(cb) cb();
            }
        });
    }
});


//Array forEachDone

Object.defineProperty(Object.prototype, "forEachDone", {
    enumerable: false,
    value: function(task, cb){
        var obj = this;
        var counter = 0;
        Object.keys(obj).forEach(function(key, index, array){
            task(obj[key], key, obj);
            if(array.length === ++counter){
                if(cb) cb();
            }
        });
    }
});

ตัวอย่าง:

var arr = ['a', 'b', 'c'];

arr.forEachDone(function(item){
    console.log(item);
}, function(){
   console.log('done');
});

// out: a b c done

การแก้ปัญหาเป็นนวัตกรรม แต่มีข้อผิดพลาดเกิดขึ้น - "งานไม่ใช่หน้าที่"
Genius

0

ฉันลองวิธีง่ายๆในการแก้ไขแบ่งปันกับคุณ:

let counter = 0;
            arr.forEach(async (item, index) => {
                await request.query(item, (err, recordset) => {
                    if (err) console.log(err);

                    //do Somthings

                    counter++;
                    if(counter == tableCmd.length){
                        sql.close();
                        callback();
                    }
                });

requestเป็น Function ของ mssql Library ในโหนด js สิ่งนี้สามารถแทนที่แต่ละฟังก์ชันหรือโค้ดที่คุณต้องการ โชคดี


0
var i=0;
const waitFor = (ms) => 
{ 
  new Promise((r) => 
  {
   setTimeout(function () {
   console.log('timeout completed: ',ms,' : ',i); 
     i++;
     if(i==data.length){
      console.log('Done')  
    }
  }, ms); 
 })
}
var data=[1000, 200, 500];
data.forEach((num) => {
  waitFor(num)
})

-2

คุณไม่จำเป็นต้องโทรกลับเพื่อทำซ้ำในรายการ เพียงเพิ่มการend()โทรหลังลูป

posts.forEach(function(v, i){
   res.write(v + ". Index " + i);
});
res.end();

3
ไม่ OP เน้นว่าตรรกะแบบอะซิงโครนัสจะดำเนินการสำหรับการวนซ้ำแต่ละครั้ง res.writeไม่ใช่การดำเนินการแบบอะซิงโครนัสดังนั้นรหัสของคุณจะไม่ทำงาน
Jim G.

-2

ทางออกที่ง่ายจะเป็นดังต่อไปนี้

function callback(){console.log("i am done");}

["a", "b", "c"].forEach(function(item, index, array){
    //code here
    if(i == array.length -1)
    callback()
}

3
ไม่ทำงานสำหรับรหัสอะซิงโครนัสซึ่งเป็นข้อสมมติฐานทั้งหมด
grg

-3

วิธีการเกี่ยวกับ setInterval ในการตรวจสอบการนับซ้ำทั้งหมดนำการรับประกัน ไม่แน่ใจว่ามันจะไม่ใช้งานเกินขอบเขตหรือไม่ แต่ฉันใช้และดูเหมือนว่าจะเป็นขอบเขต

_.forEach(actual_JSON, function (key, value) {

     // run any action and push with each iteration 

     array.push(response.id)

});


setInterval(function(){

    if(array.length > 300) {

        callback()

    }

}, 100);

มันดูเรียบง่ายแบบตรรกะ
Zeal Murapa
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.