Callback setelah semua asinkron untuk setiap callback selesai

245

Seperti judulnya. Bagaimana saya melakukan ini?

Saya ingin menelepon whenAllDone()setelah forEach-loop telah melewati setiap elemen dan melakukan beberapa pemrosesan asinkron.

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

Mungkinkah bekerja seperti ini? Kapan argumen kedua untuk forEach adalah fungsi callback yang berjalan setelah melewati semua iterasi?

Output yang diharapkan:

3 done
1 done
2 done
All done!
Dan Andreasson
sumber
13
Akan lebih baik jika forEachmetode array standar memiliki doneparameter allDonepanggilan balik dan panggilan balik!
Vanuan
22
Sangat memalukan, sesuatu yang sangat sederhana membutuhkan banyak gulat dalam JavaScript.
Ali

Jawaban:

410

Array.forEach tidak menyediakan keramahtamahan ini (oh jika itu mau) tetapi ada beberapa cara untuk mencapai apa yang Anda inginkan:

Menggunakan penghitung sederhana

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

var itemsProcessed = 0;

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

(terima kasih kepada @vanuan dan lainnya) Pendekatan ini menjamin bahwa semua item diproses sebelum memanggil callback "selesai". Anda perlu menggunakan penghitung yang diperbarui dalam panggilan balik. Tergantung pada nilai parameter indeks tidak memberikan jaminan yang sama, karena urutan pengembalian operasi asinkron tidak dijamin.

Menggunakan ES6 Janji

(perpustakaan janji dapat digunakan untuk browser lama):

  1. Memproses semua permintaan yang menjamin eksekusi secara sinkron (mis. 1 kemudian 2 kemudian 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. Memproses semua permintaan async tanpa eksekusi "sinkron" (2 mungkin selesai lebih cepat dari 1)

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

Menggunakan pustaka async

Ada perpustakaan asinkron lainnya, async menjadi yang paling populer, yang menyediakan mekanisme untuk mengekspresikan apa yang Anda inginkan.

Edit

Isi pertanyaan telah diedit untuk menghapus kode contoh yang sebelumnya sinkron, jadi saya telah memperbarui jawaban saya untuk menjelaskan. Contoh asli menggunakan kode suka sinkron untuk memodelkan perilaku asinkron, jadi yang berikut ini diterapkan:

array.forEachadalah sinkron dan begitu juga res.write, sehingga Anda hanya dapat menempatkan panggilan balik Anda setelah panggilan Anda untuk foreach:

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

  res.end();
Nick Tomlin
sumber
31
Namun, perhatikan bahwa jika ada hal-hal yang tidak sinkron di dalam forEach (mis. Anda mengulang-ulang array URL dan melakukan HTTP GET di atasnya), tidak ada jaminan bahwa res.end akan dipanggil terakhir.
AlexMA
Untuk menjalankan panggilan balik setelah tindakan async dilakukan dalam satu lingkaran, Anda dapat menggunakan masing-masing metode async utility: github.com/caolan/async#each
elkelk
2
@Vanuan saya telah memperbarui jawaban saya agar lebih cocok dengan hasil edit Anda yang cukup signifikan :)
Nick Tomlin
4
mengapa tidak adil if(index === array.length - 1)dan hapusitemsProcessed
Amin Jafari
5
@AminJafari karena panggilan asinkron mungkin tidak dapat diselesaikan sesuai urutan pendaftarannya (misalkan Anda memanggil server dan sedikit terhenti pada panggilan kedua tetapi memproses denda panggilan terakhir). Panggilan asinkron terakhir dapat diselesaikan sebelum yang sebelumnya. Mutasi seorang penjaga counter terhadap hal ini karena semua panggilan balik harus dipecat terlepas dari urutan penyelesaiannya.
Nick Tomlin
25

Jika Anda menjumpai fungsi-fungsi tidak sinkron, dan Anda ingin memastikan bahwa sebelum mengeksekusi kode itu menyelesaikan tugasnya, kami selalu dapat menggunakan kemampuan panggilan balik.

Sebagai contoh:

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

Catatan: functionAfterForEachadalah fungsi yang akan dieksekusi setelah tugas masing-masing selesai. asynchronousadalah fungsi asinkron yang dijalankan di dalam foreach.

Emil Reña Enriquez
sumber
9
Ini tidak akan berfungsi karena urutan eksekusi permintaan tidak sinkron tidak dapat dijamin. Permintaan async terakhir bisa berakhir sebelum yang lain dan menjalankan functionAfterForEach () sebelum semua permintaan dilakukan.
Rémy DAVID
@ RémyDAVID ya Anda ada benarnya mengenai urutan eksekusi atau haruskah saya katakan berapa lama proses ini selesai, javascript menjadi single threaded jadi ini akhirnya bekerja. Dan buktinya adalah jawaban yang diterima jawaban ini.
Emil Reña Enriquez
1
Saya tidak terlalu yakin mengapa Anda memiliki begitu banyak upvotes, tetapi Rémi benar. Kode Anda tidak akan berfungsi sama sekali karena asinkron berarti salah satu permintaan dapat kembali kapan saja. Meskipun JavaScript bukan multithreads, browser Anda adalah. Sangat berat, saya dapat menambahkan. Dengan demikian dapat memanggil salah satu dari panggilan balik Anda setiap saat dalam urutan apa pun tergantung pada saat balasan diterima dari server ...
Alexis Wilke
2
ya, jawaban ini sepenuhnya salah. Jika saya menjalankan 10 unduhan secara paralel, semuanya dijamin bahwa unduhan terakhir selesai sebelum sisanya dan dengan demikian mengakhiri eksekusi.
knrdk
Saya sarankan Anda menggunakan penghitung untuk menambah jumlah tugas asinkron yang diselesaikan dan mencocokkannya dengan panjang array, bukan indeks. Jumlah upvotes tidak ada hubungannya dengan bukti kebenaran jawaban.
Alex
17

Semoga ini akan memperbaiki masalah Anda, saya biasanya bekerja dengan ini ketika saya perlu menjalankan forEach dengan tugas-tugas asinkron di dalam.

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

dengan

function doAsynchronousFunction(entry,callback){
       //asynchronousjob with entry
       callback();
}
Adnene Belfodil
sumber
Saya mengalami masalah yang sama pada kode Angular 9 saya dan jawaban ini membantu saya. Meskipun jawaban @Emil Reña Enriquez juga bekerja untuk saya, tetapi saya menemukan ini sebagai jawaban yang lebih akurat dan sederhana untuk masalah ini.
omostan
17

Sungguh aneh berapa banyak jawaban yang salah telah diberikan pada kasus asinkron ! Dapat dengan mudah ditunjukkan bahwa memeriksa indeks tidak memberikan perilaku yang diharapkan:

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

keluaran:

4000 started
2000 started
1: 2000
0: 4000

Jika kami memeriksa index === array.length - 1, panggilan balik akan dipanggil setelah selesai iterasi pertama, sementara elemen pertama masih tertunda!

Untuk mengatasi masalah ini tanpa menggunakan perpustakaan eksternal seperti async, saya pikir cara terbaik Anda adalah menyimpan panjang daftar dan pengurangan jika setelah setiap iterasi. Karena hanya ada satu utas, kami yakin tidak ada kemungkinan kondisi balapan.

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);
});
Rsh
sumber
1
Itu mungkin satu-satunya solusi. Apakah pustaka async juga menggunakan penghitung?
Vanuan
1
Meskipun solusi lain melakukan pekerjaan itu, ini paling menarik karena tidak memerlukan rangkaian atau menambah kompleksitas. KISS
azatar
Harap pertimbangkan juga situasi ketika panjang array nol, dalam hal ini, callback tidak akan pernah dipanggil
Saeed Ir
6

Dengan ES2018 Anda dapat menggunakan async iterators:

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');
}
Krzysztof Grzybek
sumber
1
Tersedia dalam Node v10
Matt Swezey
2

Solusi saya tanpa Janji (ini memastikan bahwa setiap tindakan berakhir sebelum yang berikutnya dimulai):

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>

jackstrapp
sumber
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();
 });
Hardik Shimpi
sumber
1
Itu tidak akan berhasil karena jika Anda akan memiliki operasi async di dalam foreach.
Sudhanshu Gaur
0

Solusi saya:

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

Contoh:

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

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

// out: a b c done
Gabor
sumber
Solusinya inovatif tetapi kesalahan datang - "tugas bukan fungsi"
Genius
0

Saya mencoba Cara Mudah untuk mengatasinya, membagikannya kepada Anda:

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

requestadalah Fungsi Perpustakaan mssql di Node js. Ini dapat menggantikan setiap fungsi atau Kode yang Anda inginkan. Semoga berhasil

HamidReza Heydari
sumber
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)
})
Nilesh Pawar
sumber
-2

Anda seharusnya tidak memerlukan panggilan balik untuk mengulang melalui daftar. Tambahkan saja end()panggilan setelah pengulangan.

posts.forEach(function(v, i){
   res.write(v + ". Index " + i);
});
res.end();
azz
sumber
3
Tidak. OP menekankan bahwa logika asinkron akan dijalankan untuk setiap iterasi. res.writeBUKAN operasi asinkron, jadi kode Anda tidak akan berfungsi.
Jim G.
-2

Solusi sederhana akan seperti ikuti

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

["a", "b", "c"].forEach(function(item, index, array){
    //code here
    if(i == array.length -1)
    callback()
}
molham556
sumber
3
Tidak berfungsi untuk kode asinkron yang merupakan keseluruhan premis dari pertanyaan.
grg
-3

Bagaimana dengan setInterval, untuk memeriksa jumlah iterasi yang lengkap, membawa jaminan. tidak yakin apakah itu tidak membebani ruang lingkup, tetapi saya menggunakannya dan tampaknya menjadi satu-satunya

_.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);
Tino Costa 'El Nino'
sumber
Ini terlihat sederhana secara logis
Semangat Murapa