Cara yang benar untuk menulis loop untuk janji.


116

Bagaimana cara membuat loop dengan benar untuk memastikan panggilan janji berikut dan logger.log (res) yang dirantai berjalan secara sinkron melalui iterasi? (burung biru)

db.getUser(email).then(function(res) { logger.log(res); }); // this is a promise

Saya mencoba cara berikut (metode dari http://blog.victorquinn.com/javascript-promise- While-loop )

var Promise = require('bluebird');

var promiseWhile = function(condition, action) {
    var resolver = Promise.defer();

    var loop = function() {
        if (!condition()) return resolver.resolve();
        return Promise.cast(action())
            .then(loop)
            .catch(resolver.reject);
    };

    process.nextTick(loop);

    return resolver.promise;
});

var count = 0;
promiseWhile(function() {
    return count < 10;
}, function() {
    return new Promise(function(resolve, reject) {
        db.getUser(email)
          .then(function(res) { 
              logger.log(res); 
              count++;
              resolve();
          });
    }); 
}).then(function() {
    console.log('all done');
}); 

Meskipun tampaknya berhasil, tetapi menurut saya ini tidak menjamin urutan pemanggilan logger.log (res);

Ada saran?


1
Kode tersebut terlihat bagus bagi saya (rekursi dengan loopfungsi tersebut adalah cara untuk melakukan loop sinkron). Menurut Anda, mengapa tidak ada jaminan?
hugomg

db.getUser (email) dijamin akan dipanggil secara berurutan. Namun, karena db.getUser () itu sendiri adalah sebuah promise, memanggilnya secara berurutan tidak selalu berarti kueri database untuk 'email' berjalan secara berurutan karena fitur janji asinkron. Dengan demikian, logger.log (res) dipanggil tergantung pada kueri mana yang selesai pertama kali.
pengguna2127480

1
@ user2127480: Tapi iterasi loop berikutnya dipanggil secara berurutan hanya setelah promise diselesaikan, begitulah cara whilekerja kode itu?
Bergi

Jawaban:


78

Saya rasa ini tidak menjamin urutan pemanggilan logger.log (res);

Sebenarnya, memang begitu. Pernyataan itu dijalankan sebelum resolvepanggilan.

Ada saran?

Banyak. Yang paling penting adalah penggunaan antipattern buat-janji-manual - lakukan saja

promiseWhile(…, function() {
    return db.getUser(email)
             .then(function(res) { 
                 logger.log(res); 
                 count++;
             });
})…

Kedua, whilefungsi itu bisa banyak disederhanakan:

var promiseWhile = Promise.method(function(condition, action) {
    if (!condition()) return;
    return action().then(promiseWhile.bind(null, condition, action));
});

Ketiga, saya tidak akan menggunakan whileloop (dengan variabel penutupan) tetapi forloop:

var promiseFor = Promise.method(function(condition, action, value) {
    if (!condition(value)) return value;
    return action(value).then(promiseFor.bind(null, condition, action));
});

promiseFor(function(count) {
    return count < 10;
}, function(count) {
    return db.getUser(email)
             .then(function(res) { 
                 logger.log(res); 
                 return ++count;
             });
}, 0).then(console.log.bind(console, 'all done'));

2
Ups. Kecuali yang actionmengambil valuesebagai argumen dalam promiseFor. JADI tidak akan membiarkan saya melakukan pengeditan sekecil itu. Terima kasih, ini sangat membantu dan elegan.
Gordon

1
@ Roamer-1888: Mungkin terminologinya agak aneh, tapi maksud saya bahwa sebuah whileloop menguji beberapa status global sementara sebuah forloop memiliki variabel iterasinya (penghitung) yang terikat ke badan loop itu sendiri. Sebenarnya saya telah menggunakan pendekatan yang lebih fungsional yang terlihat lebih seperti iterasi fixpoint daripada loop. Periksa kembali kode mereka, valueparameternya berbeda.
Bergi

2
Oke, saya melihatnya sekarang. Karena .bind()mengaburkan yang baru value, saya pikir saya mungkin memilih untuk menggunakan fungsi tersebut agar mudah dibaca. Dan maaf jika saya terlalu tebal tetapi jika promiseFordan promiseWhiletidak hidup berdampingan, lalu bagaimana yang satu memanggil yang lain?
Roamer-1888

2
@herve Pada dasarnya Anda dapat menghilangkannya dan mengganti return …oleh return Promise.resolve(…). Jika Anda membutuhkan perlindungan tambahan terhadap conditionatau actionmelempar pengecualian (seperti Promise.methodmenyediakannya ), bungkus seluruh badan fungsi dalam areturn Promise.resolve().then(() => { … })
Bergi

2
@herve Sebenarnya itu harus Promise.resolve().then(action).…atau Promise.resolve(action()).…, Anda tidak perlu membungkus nilai pengembalianthen
Bergi

134

Jika Anda benar-benar menginginkan seorang jenderal promiseWhen() fungsi untuk ini dan tujuan lainnya, maka lakukanlah dengan menggunakan penyederhanaan Bergi. Namun, karena cara promise berfungsi, meneruskan callback dengan cara ini umumnya tidak diperlukan dan memaksa Anda untuk melewati rintangan kecil yang rumit.

Sejauh yang saya tahu Anda sedang mencoba:

  • untuk mengambil rangkaian detail pengguna secara asinkron untuk kumpulan alamat email (setidaknya, itulah satu-satunya skenario yang masuk akal).
  • melakukannya dengan membangun .then()rantai melalui rekursi.
  • untuk mempertahankan urutan asli saat menangani hasil yang dikembalikan.

Didefinisikan demikian, masalahnya sebenarnya adalah yang dibahas dalam "The Collection Kerfuffle" di Promise Anti-patterns , yang menawarkan dua solusi sederhana:

  • panggilan asynchronous paralel menggunakan Array.prototype.map()
  • panggilan asynchronous serial menggunakan Array.prototype.reduce().

Pendekatan paralel akan (secara langsung) memberikan masalah yang Anda coba hindari - bahwa urutan tanggapan tidak pasti. Pendekatan serial akan membangun .then()rantai yang diperlukan - datar - tanpa rekursi.

function fetchUserDetails(arr) {
    return arr.reduce(function(promise, email) {
        return promise.then(function() {
            return db.getUser(email).done(function(res) {
                logger.log(res);
            });
        });
    }, Promise.resolve());
}

Hubungi sebagai berikut:

//Compose here, by whatever means, an array of email addresses.
var arrayOfEmailAddys = [...];

fetchUserDetails(arrayOfEmailAddys).then(function() {
    console.log('all done');
});

Seperti yang Anda lihat, tidak perlu var luar yang jelek countatau conditionfungsi terkaitnya . Batas (dari 10 dalam pertanyaan) ditentukan seluruhnya oleh panjang larik arrayOfEmailAddys.


16
rasanya ini harus menjadi jawaban yang dipilih. pendekatan yang anggun dan sangat dapat digunakan kembali.
ken

1
Adakah yang tahu jika hasil tangkapan akan merambat kembali ke induknya? Misalnya jika db.getUser gagal, akankah kesalahan (reject) menyebar kembali?
wayofthefuture

@way_future, no. Pikirkan seperti ini ..... Anda tidak dapat mengubah sejarah.
Roamer-1888

4
Terima kasih atas jawabannya. Ini harus menjadi jawaban yang diterima.
klvs

1
@ Roamer-1888 Kesalahan saya, saya salah membaca pertanyaan awal. Saya (secara pribadi) sedang mencari solusi di mana daftar awal yang Anda butuhkan untuk dikurangi tumbuh saat permintaan Anda diselesaikan (ini adalah queryMore of a DB). Dalam hal ini saya menemukan ide untuk menggunakan reduce dengan generator sebuah pemisahan yang cukup bagus dari (1) perpanjangan bersyarat dari rantai perjanjian dan (2) konsumsi hasil yang dikembalikan.
jhp

40

Inilah cara saya melakukannya dengan objek Janji standar.

// Given async function sayHi
function sayHi() {
  return new Promise((resolve) => {
    setTimeout(() => {
      console.log('Hi');
      resolve();
    }, 3000);
  });
}

// And an array of async functions to loop through
const asyncArray = [sayHi, sayHi, sayHi];

// We create the start of a promise chain
let chain = Promise.resolve();

// And append each function in the array to the promise chain
for (const func of asyncArray) {
  chain = chain.then(func);
}

// Output:
// Hi
// Hi (After 3 seconds)
// Hi (After 3 more seconds)

Jawaban hebat @youngwerth
Jam Risser

3
bagaimana cara mengirim params dengan cara ini?
Akash khan

4
@khan on the chain = chain.then (func) line, Anda dapat melakukan: chain = chain.then(func.bind(null, "...your params here")); atau chain = chain.then(() => func("your params here"));
youngwerth

9

Diberikan

  • fungsi asyncFn
  • berbagai item

Yg dibutuhkan

  • promise chaining .then () dalam seri (dalam urutan)
  • es6 asli

Larutan

let asyncFn = (item) => {
  return new Promise((resolve, reject) => {
    setTimeout( () => {console.log(item); resolve(true)}, 1000 )
  })
}

// asyncFn('a')
// .then(()=>{return async('b')})
// .then(()=>{return async('c')})
// .then(()=>{return async('d')})

let a = ['a','b','c','d']

a.reduce((previous, current, index, array) => {
  return previous                                    // initiates the promise chain
  .then(()=>{return asyncFn(array[index])})      //adds .then() promise for each item
}, Promise.resolve())

2
Jika asyncakan menjadi kata yang dicadangkan dalam JavaScript, mungkin menambahkan kejelasan untuk mengganti nama fungsi itu di sini.
hippietrail

Juga, bukankah panah gemuk berfungsi tanpa tubuh di kawat gigi hanya mengembalikan ekspresi yang dievaluasi di sana? Itu akan membuat kodenya lebih ringkas. Saya juga dapat menambahkan komentar yang menyatakan bahwa currenttidak digunakan.
hippietrail

2
ini cara yang tepat!
teleme.io

4

Ada cara baru untuk menyelesaikan ini dan itu dengan menggunakan async / await.

async function myFunction() {
  while(/* my condition */) {
    const res = await db.getUser(email);
    logger.log(res);
  }
}

myFunction().then(() => {
  /* do other stuff */
})

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function https://ponyfoo.com/articles/understanding-javascript-async-await


Terima kasih, ini tidak melibatkan penggunaan framework (bluebird).
Rolf

3

Fungsi yang disarankan Bergi sangat bagus:

var promiseWhile = Promise.method(function(condition, action) {
      if (!condition()) return;
    return action().then(promiseWhile.bind(null, condition, action));
});

Saya tetap ingin menambahkan sedikit, yang masuk akal, saat menggunakan promise:

var promiseWhile = Promise.method(function(condition, action, lastValue) {
  if (!condition()) return lastValue;
  return action().then(promiseWhile.bind(null, condition, action));
});

Dengan cara ini while loop bisa disematkan ke dalam rantai janji dan diselesaikan dengan lastValue (juga jika action () tidak pernah dijalankan). Lihat contoh:

var count = 10;
util.promiseWhile(
  function condition() {
    return count > 0;
  },
  function action() {
    return new Promise(function(resolve, reject) {
      count = count - 1;
      resolve(count)
    })
  },
  count)

3

Saya akan membuat sesuatu seperti ini:

var request = []
while(count<10){
   request.push(db.getUser(email).then(function(res) { return res; }));
   count++
};

Promise.all(request).then((dataAll)=>{
  for (var i = 0; i < dataAll.length; i++) {

      logger.log(dataAll[i]); 
  }  
});

dengan cara ini, dataAll adalah larik terurut dari semua elemen untuk dicatat. Dan operasi log akan dilakukan ketika semua janji sudah selesai.


Promise.all akan memanggil janji panggilan wasiat pada saat yang sama. Jadi urutan penyelesaiannya mungkin berubah. Pertanyaan itu menanyakan janji yang dirantai. Jadi urutan penyelesaiannya tidak boleh diubah.
canbax

Edit 1: Anda tidak perlu menelepon Promise.all sama sekali. Selama janji-janji itu dicabut, mereka akan dieksekusi secara paralel.
canbax

1

Gunakan async dan await (es6):

function taskAsync(paramets){
 return new Promise((reslove,reject)=>{
 //your logic after reslove(respoce) or reject(error)
})
}

async function fName(){
let arry=['list of items'];
  for(var i=0;i<arry.length;i++){
   let result=await(taskAsync('parameters'));
}

}

0
function promiseLoop(promiseFunc, paramsGetter, conditionChecker, eachFunc, delay) {
    function callNext() {
        return promiseFunc.apply(null, paramsGetter())
            .then(eachFunc)
    }

    function loop(promise, fn) {
        if (delay) {
            return new Promise(function(resolve) {
                setTimeout(function() {
                    resolve();
                }, delay);
            })
                .then(function() {
                    return promise
                        .then(fn)
                        .then(function(condition) {
                            if (!condition) {
                                return true;
                            }
                            return loop(callNext(), fn)
                        })
                });
        }
        return promise
            .then(fn)
            .then(function(condition) {
                if (!condition) {
                    return true;
                }
                return loop(callNext(), fn)
            })
    }

    return loop(callNext(), conditionChecker);
}


function makeRequest(param) {
    return new Promise(function(resolve, reject) {
        var req = https.request(function(res) {
            var data = '';
            res.on('data', function (chunk) {
                data += chunk;
            });
            res.on('end', function () {
                resolve(data);
            });
        });
        req.on('error', function(e) {
            reject(e);
        });
        req.write(param);
        req.end();
    })
}

function getSomething() {
    var param = 0;

    var limit = 10;

    var results = [];

    function paramGetter() {
        return [param];
    }
    function conditionChecker() {
        return param <= limit;
    }
    function callback(result) {
        results.push(result);
        param++;
    }

    return promiseLoop(makeRequest, paramGetter, conditionChecker, callback)
        .then(function() {
            return results;
        });
}

getSomething().then(function(res) {
    console.log('results', res);
}).catch(function(err) {
    console.log('some error along the way', err);
});

0

Bagaimana kalau yang ini menggunakan BlueBird ?

function fetchUserDetails(arr) {
    return Promise.each(arr, function(email) {
        return db.getUser(email).done(function(res) {
            logger.log(res);
        });
    });
}

0

Berikut metode lain (ES6 w / std Promise). Menggunakan kriteria keluar tipe lodash / garis bawah (return === false). Perhatikan bahwa Anda dapat dengan mudah menambahkan metode exitIf () dalam opsi untuk dijalankan di doOne ().

const whilePromise = (fnReturningPromise,options = {}) => { 
    // loop until fnReturningPromise() === false
    // options.delay - setTimeout ms (set to 0 for 1 tick to make non-blocking)
    return new Promise((resolve,reject) => {
        const doOne = () => {
            fnReturningPromise()
            .then((...args) => {
                if (args.length && args[0] === false) {
                    resolve(...args);
                } else {
                    iterate();
                }
            })
        };
        const iterate = () => {
            if (options.delay !== undefined) {
                setTimeout(doOne,options.delay);
            } else {
                doOne();
            }
        }
        Promise.resolve()
        .then(iterate)
        .catch(reject)
    })
};

0

Menggunakan objek janji standar, dan membuat janji mengembalikan hasilnya.

function promiseMap (data, f) {
  const reducer = (promise, x) =>
    promise.then(acc => f(x).then(y => acc.push(y) && acc))
  return data.reduce(reducer, Promise.resolve([]))
}

var emails = []

function getUser(email) {
  return db.getUser(email)
}

promiseMap(emails, getUser).then(emails => {
  console.log(emails)
})

0

Pertama, ambil array promise (promise array) dan setelah selesaikan array promise ini menggunakan Promise.all(promisearray).

var arry=['raju','ram','abdul','kruthika'];

var promiseArry=[];
for(var i=0;i<arry.length;i++) {
  promiseArry.push(dbFechFun(arry[i]));
}

Promise.all(promiseArry)
  .then((result) => {
    console.log(result);
  })
  .catch((error) => {
     console.log(error);
  });

function dbFetchFun(name) {
  // we need to return a  promise
  return db.find({name:name}); // any db operation we can write hear
}
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.