Node JS Promise.all dan forEach


120

Saya memiliki struktur seperti array yang memperlihatkan metode async. Metode asinkron memanggil struktur larik yang dikembalikan yang pada gilirannya mengekspos lebih banyak metode asinkron. Saya membuat objek JSON lain untuk menyimpan nilai yang diperoleh dari struktur ini, jadi saya harus berhati-hati dalam melacak referensi di callback.

Saya telah membuat kode solusi brute force, tetapi saya ingin mempelajari solusi yang lebih idiomatis atau bersih.

  1. Pola tersebut harus dapat diulang untuk n tingkat penumpukan.
  2. Saya perlu menggunakan promise.all atau beberapa teknik serupa untuk menentukan kapan harus menyelesaikan rutinitas penutup.
  3. Tidak setiap elemen harus melibatkan pembuatan panggilan asinkron. Jadi dalam janji bersarang saya tidak bisa begitu saja membuat tugas ke elemen array JSON saya berdasarkan indeks. Namun demikian, saya perlu menggunakan sesuatu seperti promise.all di forEach bersarang untuk memastikan bahwa semua penugasan properti telah dilakukan sebelum menyelesaikan rutinitas pelampiran.
  4. Saya menggunakan lib promise bluebird tetapi ini bukan persyaratan

Berikut adalah beberapa kode parsial -

var jsonItems = [];

items.forEach(function(item){

  var jsonItem = {};
  jsonItem.name = item.name;
  item.getThings().then(function(things){
  // or Promise.all(allItemGetThingCalls, function(things){

    things.forEach(function(thing, index){

      jsonItems[index].thingName = thing.name;
      if(thing.type === 'file'){

        thing.getFile().then(function(file){ //or promise.all?

          jsonItems[index].filesize = file.getSize();

Ini adalah tautan ke sumber kerja yang ingin saya tingkatkan. github.com/pebanfield/change-view-service/blob/master/src/…
pengguna3205931

1
Saya melihat dalam sampel Anda menggunakan bluebird, bluebird sebenarnya membuat hidup Anda lebih mudah dengan Promise.map(bersamaan) dan Promise.each(berurutan) dalam hal ini, juga catatan Promise.defertidak digunakan lagi - kode dalam jawaban saya menunjukkan cara menghindarinya dengan mengembalikan janji. Janji adalah tentang nilai kembali.
Benjamin Gruenbaum

Jawaban:


368

Ini cukup mudah dengan beberapa aturan sederhana:

  • Kapan pun Anda membuat janji di sebuah then, kembalikan - janji apa pun yang tidak Anda kembalikan tidak akan ditunggu di luar.
  • Setiap kali Anda membuat beberapa janji, .allitu - dengan cara itu menunggu semua janji dan tidak ada kesalahan dari salah satunya yang dibungkam.
  • Kapanpun Anda bersarang then, Anda biasanya dapat kembali ke tengah - thenrantai biasanya sedalam paling banyak 1 tingkat.
  • Kapan pun Anda melakukan IO, itu harus dengan janji - baik itu harus dalam janji atau harus menggunakan janji untuk menandakan penyelesaiannya.

Dan beberapa tip:

  • Pemetaan lebih baik dilakukan dengan .map daripada denganfor/push - jika Anda memetakan nilai dengan suatu fungsi, mapmemungkinkan Anda secara ringkas mengekspresikan gagasan tentang menerapkan tindakan satu per satu dan menggabungkan hasilnya.
  • Concurrency lebih baik daripada eksekusi berurutan jika gratis - lebih baik mengeksekusi sesuatu secara bersamaan dan menunggu Promise.alldaripada mengeksekusi sesuatu satu demi satu - masing-masing menunggu sebelum yang berikutnya.

Oke, jadi mari kita mulai:

var items = [1, 2, 3, 4, 5];
var fn = function asyncMultiplyBy2(v){ // sample async action
    return new Promise(resolve => setTimeout(() => resolve(v * 2), 100));
};
// map over forEach since it returns

var actions = items.map(fn); // run the function over all items

// we now have a promises array and we want to wait for it

var results = Promise.all(actions); // pass array of promises

results.then(data => // or just .then(console.log)
    console.log(data) // [2, 4, 6, 8, 10]
);

// we can nest this of course, as I said, `then` chains:

var res2 = Promise.all([1, 2, 3, 4, 5].map(fn)).then(
    data => Promise.all(data.map(fn))
).then(function(data){
    // the next `then` is executed after the promise has returned from the previous
    // `then` fulfilled, in this case it's an aggregate promise because of 
    // the `.all` 
    return Promise.all(data.map(fn));
}).then(function(data){
    // just for good measure
    return Promise.all(data.map(fn));
});

// now to get the results:

res2.then(function(data){
    console.log(data); // [16, 32, 48, 64, 80]
});

5
Ah, beberapa aturan dari sudut pandang Anda :-)
Bergi

1
@Bergi seseorang harus benar-benar membuat daftar aturan ini dan latar belakang singkat tentang promise. Kita mungkin bisa menyimpannya di bluebirdjs.com.
Benjamin Gruenbaum

karena saya tidak seharusnya hanya mengucapkan terima kasih - contoh ini terlihat bagus dan saya menyukai saran peta, namun, apa yang harus dilakukan tentang kumpulan objek di mana hanya beberapa yang memiliki metode async? (Poin saya 3 di atas) Saya punya ide bahwa saya akan mengabstraksi logika parsing untuk setiap elemen menjadi sebuah fungsi dan kemudian menyelesaikannya baik pada respons panggilan async atau di mana tidak ada panggilan asinkron yang diselesaikan dengan mudah. Apakah itu masuk akal?
user3205931

Saya juga perlu memiliki fungsi peta yang mengembalikan objek json yang sedang saya bangun dan hasil panggilan async. Saya perlu memastikannya jadi tidak yakin bagaimana melakukannya - akhirnya semuanya harus rekursif karena saya sedang menjalankan direktori struktur - Saya masih mengunyah ini tetapi pekerjaan berbayar menghalangi :(
user3205931

2
@ user3205931 janji itu sederhana, bukan mudah , yaitu - janji itu tidak seakrab hal lain tetapi begitu Anda melakukannya, janji itu jauh lebih baik untuk digunakan. Bersabarlah Anda akan mendapatkannya :)
Benjamin Gruenbaum

42

Berikut adalah contoh sederhana menggunakan reduce. Ini berjalan secara serial, memelihara urutan penyisipan, dan tidak memerlukan Bluebird.

/**
 * 
 * @param items An array of items.
 * @param fn A function that accepts an item from the array and returns a promise.
 * @returns {Promise}
 */
function forEachPromise(items, fn) {
    return items.reduce(function (promise, item) {
        return promise.then(function () {
            return fn(item);
        });
    }, Promise.resolve());
}

Dan gunakan seperti ini:

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

function logItem(item) {
    return new Promise((resolve, reject) => {
        process.nextTick(() => {
            console.log(item);
            resolve();
        })
    });
}

forEachPromise(items, logItem).then(() => {
    console.log('done');
});

Kami merasa berguna untuk mengirim konteks opsional ke dalam loop. Konteksnya opsional dan dibagikan oleh semua iterasi.

function forEachPromise(items, fn, context) {
    return items.reduce(function (promise, item) {
        return promise.then(function () {
            return fn(item, context);
        });
    }, Promise.resolve());
}

Fungsi promise Anda akan terlihat seperti ini:

function logItem(item, context) {
    return new Promise((resolve, reject) => {
        process.nextTick(() => {
            console.log(item);
            context.itemCount++;
            resolve();
        })
    });
}

Terima kasih untuk ini - solusi Anda telah berhasil untuk saya di mana orang lain (termasuk berbagai npm libs) belum. Apakah ini sudah dipublikasikan ke npm?
SamF

Terima kasih. Fungsi mengasumsikan semua Janji diselesaikan. Bagaimana kita menangani janji yang ditolak? Juga, bagaimana kita menangani janji yang berhasil dengan sebuah nilai?
oyalhi

@oyalhi Saya akan menyarankan menggunakan 'konteks' dan menambahkan array parameter masukan yang ditolak yang dipetakan ke kesalahan. Ini benar-benar per kasus penggunaan, karena beberapa akan ingin mengabaikan semua janji yang tersisa dan beberapa tidak. Untuk nilai yang dikembalikan, Anda juga bisa menggunakan pendekatan serupa.
Steven Spungin

1

Saya telah melalui situasi yang sama. Saya menyelesaikannya menggunakan dua Promise.All ().

Saya pikir itu solusi yang sangat bagus, jadi saya menerbitkannya di npm: https://www.npmjs.com/package/promise-foreach

Saya pikir kode Anda akan menjadi seperti ini

var promiseForeach = require('promise-foreach')
var jsonItems = [];
promiseForeach.each(jsonItems,
    [function (jsonItems){
        return new Promise(function(resolve, reject){
            if(jsonItems.type === 'file'){
                jsonItems.getFile().then(function(file){ //or promise.all?
                    resolve(file.getSize())
                })
            }
        })
    }],
    function (result, current) {
        return {
            type: current.type,
            size: jsonItems.result[0]
        }
    },
    function (err, newList) {
        if (err) {
            console.error(err)
            return;
        }
        console.log('new jsonItems : ', newList)
    })

0

Hanya untuk menambah solusi yang disajikan, dalam kasus saya, saya ingin mengambil banyak data dari Firebase untuk daftar produk. Inilah cara saya melakukannya:

useEffect(() => {
  const fn = p => firebase.firestore().doc(`products/${p.id}`).get();
  const actions = data.occasion.products.map(fn);
  const results = Promise.all(actions);
  results.then(data => {
    const newProducts = [];
    data.forEach(p => {
      newProducts.push({ id: p.id, ...p.data() });
    });
    setProducts(newProducts);
  });
}, [data]);
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.