Selesaikan satu demi satu janji (yaitu secara berurutan)?


269

Pertimbangkan kode berikut yang membaca larik file secara serial / berurutan. readFilesmengembalikan janji, yang diselesaikan hanya setelah semua file dibaca secara berurutan.

var readFile = function(file) {
  ... // Returns a promise.
};

var readFiles = function(files) {
  return new Promise((resolve, reject) => 

    var readSequential = function(index) {
      if (index >= files.length) {
        resolve();
      } else {
        readFile(files[index]).then(function() {
          readSequential(index + 1);
        }).catch(reject);
      }
    };

   readSequential(0); // Start!

  });
};

Kode di atas berfungsi, tetapi saya tidak suka harus melakukan rekursi untuk hal-hal yang terjadi secara berurutan. Apakah ada cara yang lebih sederhana bahwa kode ini dapat ditulis ulang sehingga saya tidak harus menggunakan yang anehreadSequential fungsi ?

Awalnya saya mencoba menggunakan Promise.all, tetapi itu menyebabkan semua readFilepanggilan terjadi secara bersamaan, yang bukan yang saya inginkan:

var readFiles = function(files) {
  return Promise.all(files.map(function(file) {
    return readFile(file);
  }));
};

2
Apa pun yang harus menunggu operasi asinkron sebelumnya selesai harus dilakukan dalam panggilan balik. Menggunakan janji tidak mengubah itu. Jadi Anda perlu rekursi.
Barmar

1
FYI, ini bukan rekursi teknis karena tidak ada susunan bingkai tumpukan. Sebelumnya readFileSequential()sudah kembali sebelum yang berikutnya dipanggil (karena async, selesai lama setelah panggilan fungsi asli sudah kembali).
jfriend00

1
@ jfriend00 Akumulasi bingkai tumpukan tidak diperlukan untuk rekursi - hanya referensi sendiri. Ini hanya teknis saja.
Benjamin Gruenbaum

3
@BenjaminGruenbaum - maksud saya adalah sama sekali tidak ada yang salah dengan memiliki fungsi memanggil dirinya sendiri untuk memulai iterasi berikutnya. Tidak ada kerugian untuk itu dan, pada kenyataannya, ini adalah cara yang efisien untuk mengurutkan operasi async. Jadi, tidak ada alasan untuk menghindari sesuatu yang tampak seperti rekursi. Ada solusi rekursif untuk beberapa masalah yang tidak efisien - ini bukan salah satunya.
jfriend00

1
Hai, per diskusi dan permintaan di ruang JavaScript saya sudah mengedit jawaban ini sehingga kami dapat mengarahkan orang lain sebagai kanonik. Jika Anda tidak setuju, beri tahu saya dan saya akan memulihkannya dan membuka yang terpisah.
Benjamin Gruenbaum

Jawaban:


337

Pembaruan 2017 : Saya akan menggunakan fungsi async jika lingkungan mendukungnya:

async function readFiles(files) {
  for(const file of files) {
    await readFile(file);
  }
};

Jika mau, Anda dapat menunda membaca file sampai Anda membutuhkannya menggunakan generator async (jika lingkungan Anda mendukungnya):

async function* readFiles(files) {
  for(const file of files) {
    yield await readFile(file);
  }
};

Perbarui: Dalam pemikiran kedua - saya mungkin menggunakan for loop sebagai gantinya:

var readFiles = function(files) {
  var p = Promise.resolve(); // Q() in q

  files.forEach(file =>
      p = p.then(() => readFile(file)); 
  );
  return p;
};

Atau lebih kompak, dengan mengurangi:

var readFiles = function(files) {
  return files.reduce((p, file) => {
     return p.then(() => readFile(file));
  }, Promise.resolve()); // initial
};

Di perpustakaan janji lain (seperti kapan dan Bluebird) Anda memiliki metode utilitas untuk ini.

Misalnya, Bluebird adalah:

var Promise = require("bluebird");
var fs = Promise.promisifyAll(require("fs"));

var readAll = Promise.resolve(files).map(fs.readFileAsync,{concurrency: 1 });
// if the order matters, you can use Promise.each instead and omit concurrency param

readAll.then(function(allFileContents){
    // do stuff to read files.
});

Meskipun sebenarnya tidak ada alasan untuk tidak menggunakan async menunggu hari ini.


2
@ EmreTapcı, tidak. Fungsi panah "=>" sudah menunjukkan pengembalian.
Maks

Jika Anda menggunakan TypeScript, saya pikir solusi loop "for in" adalah yang terbaik. Mengurangi Pengembalian Janji rekursif misalnya. tipe panggilan balik yang pertama adalah Janji <void>, lalu yang kedua adalah Janji <Janji <void>> dan seterusnya - tidak mungkin untuk mengetik tanpa menggunakan apa pun yang saya pikir
Artur Tagisow

@ArturTagisow TypeScript (setidaknya versi baru) memiliki tipe rekursif dan harus menyelesaikan jenis dengan benar di sini. Tidak ada yang namanya Janji <Janji <T>> karena janji "berasimilasi secara rekursif". Promise.resolve(Promise.resolve(15))identik dengan Promise.resolve(15).
Benjamin Gruenbaum


72

Inilah cara saya lebih suka menjalankan tugas secara seri.

function runSerial() {
    var that = this;
    // task1 is a function that returns a promise (and immediately starts executing)
    // task2 is a function that returns a promise (and immediately starts executing)
    return Promise.resolve()
        .then(function() {
            return that.task1();
        })
        .then(function() {
            return that.task2();
        })
        .then(function() {
            console.log(" ---- done ----");
        });
}

Bagaimana dengan kasing dengan lebih banyak tugas? Seperti 10?

function runSerial(tasks) {
  var result = Promise.resolve();
  tasks.forEach(task => {
    result = result.then(() => task());
  });
  return result;
}

8
Dan bagaimana dengan kasus di mana Anda tidak tahu jumlah tugas yang tepat?
damd

1
Dan bagaimana dengan ketika Anda tahu jumlah tugas, tetapi hanya saat runtime?
joeytwiddle

10
"Anda sama sekali tidak ingin beroperasi di atas sederetan janji. Per spek janji, segera setelah sebuah janji dibuat, itu mulai dijalankan. Jadi yang Anda inginkan adalah sederetan pabrik janji" lihat kesalahan lanjut # 3 di sini: pouchdb.com/2015/05/18/we-have-a-problem-with-promises.html
edelans

5
Jika Anda mengurangi kebisingan garis, Anda juga dapat menulisresult = result.then(task);
Daniel Buckmaster

1
@DanielBuckmaster ya, tapi hati-hati, karena jika tugas () mengembalikan nilai, itu akan diteruskan ke doa berikutnya. Jika tugas Anda memiliki argumen opsional, ini dapat menyebabkan efek samping. Kode saat ini menelan hasil dan secara eksplisit memanggil tugas berikutnya tanpa argumen.
JHH

63

Pertanyaan ini sudah lama, tetapi kita hidup di dunia ES6 dan JavaScript fungsional, jadi mari kita lihat bagaimana kita dapat meningkatkannya.

Karena janji segera dieksekusi, kita tidak bisa begitu saja membuat serangkaian janji, semuanya akan diluncurkan secara paralel.

Sebaliknya, kita perlu membuat berbagai fungsi yang mengembalikan janji. Setiap fungsi kemudian akan dieksekusi secara berurutan, yang kemudian memulai janji di dalamnya.

Kita dapat memecahkan ini beberapa cara, tetapi cara favorit saya adalah menggunakan reduce .

Ini menjadi sedikit rumit menggunakan reduce dalam kombinasi dengan janji-janji, jadi saya telah memecah satu liner menjadi beberapa gigitan yang lebih kecil yang dapat dicerna di bawah ini.

Inti dari fungsi ini adalah menggunakan reducedimulai dengan nilai awal dariPromise.resolve([]) , atau janji yang berisi array kosong.

Janji ini kemudian akan diteruskan ke reducemetode sebagai promise. Ini adalah kunci untuk menghubungkan setiap janji bersama secara berurutan. Janji berikutnya untuk mengeksekusi adalah funcdan ketika thenkebakaran, hasilnya disatukan dan janji itu kemudian dikembalikan, mengeksekusireduce siklus dengan fungsi janji berikutnya.

Setelah semua janji telah dieksekusi, janji yang dikembalikan akan berisi berbagai hasil dari setiap janji.

Contoh ES6 (satu liner)

/*
 * serial executes Promises sequentially.
 * @param {funcs} An array of funcs that return promises.
 * @example
 * const urls = ['/url1', '/url2', '/url3']
 * serial(urls.map(url => () => $.ajax(url)))
 *     .then(console.log.bind(console))
 */
const serial = funcs =>
    funcs.reduce((promise, func) =>
        promise.then(result => func().then(Array.prototype.concat.bind(result))), Promise.resolve([]))

Contoh ES6 (rusak)

// broken down to for easier understanding

const concat = list => Array.prototype.concat.bind(list)
const promiseConcat = f => x => f().then(concat(x))
const promiseReduce = (acc, x) => acc.then(promiseConcat(x))
/*
 * serial executes Promises sequentially.
 * @param {funcs} An array of funcs that return promises.
 * @example
 * const urls = ['/url1', '/url2', '/url3']
 * serial(urls.map(url => () => $.ajax(url)))
 *     .then(console.log.bind(console))
 */
const serial = funcs => funcs.reduce(promiseReduce, Promise.resolve([]))

Pemakaian:

// first take your work
const urls = ['/url1', '/url2', '/url3', '/url4']

// next convert each item to a function that returns a promise
const funcs = urls.map(url => () => $.ajax(url))

// execute them serially
serial(funcs)
    .then(console.log.bind(console))

1
sangat bagus, terima kasih, Array.prototype.concat.bind(result)adalah bagian yang saya lewatkan, harus melakukan mendorong ke hasil secara manual yang bekerja tetapi kurang keren
zavr

Karena kita semua tentang JS modern, saya percaya console.log.bind(console)pernyataan dalam contoh terakhir Anda sekarang biasanya tidak perlu. Hari-hari ini Anda bisa lulus begitu saja console.log. Misalnya. serial(funcs).then(console.log). Diuji pada simpul saat ini dan Chrome.
Molomby

Ini agak sulit untuk membungkus kepalaku tetapi pengurangan pada dasarnya melakukan ini dengan benar? Promise.resolve([]).then((x) => { const data = mockApi('/data/1'); return Promise.resolve(x.concat(data)) }).then((x) => { const data = mockApi('/data/2'); return Promise.resolve(x.concat(data)); });
danecando

@danecando, ya ini terlihat benar. Anda juga dapat membatalkan Janji. Selesaikan sebagai balasannya, nilai apa pun yang dikembalikan akan secara otomatis diselesaikan kecuali Anda memanggil Janji.
joelnet

@ joelnet, sebagai tanggapan atas komentar danecando, saya pikir pengurangan apa yang harus lebih tepat diungkapkan dalam ungkapan berikut, apakah Anda setuju? Promise.resolve([]).then(x => someApiCall('url1').then(r => x.concat(r))).then(x => someApiCall('url2').then(r => x.concat(r)))dan sebagainya
bufferoverflow76

37

Untuk melakukan ini cukup di ES6:

function(files) {
  // Create a new empty promise (don't do that with real people ;)
  var sequence = Promise.resolve();

  // Loop over each file, and add on a promise to the
  // end of the 'sequence' promise.
  files.forEach(file => {

    // Chain one computation onto the sequence
    sequence = 
      sequence
        .then(() => performComputation(file))
        .then(result => doSomething(result)); 
        // Resolves for each file, one at a time.

  })

  // This will resolve after the entire chain is resolved
  return sequence;
}

1
Tampaknya menggunakan garis bawah. Anda dapat menyederhanakan files.forEachjika file adalah array.
Gustavo Rodrigues

2
Yah ... itu ES5. Cara ES6 akan menjadi for (file of files) {...}.
Gustavo Rodrigues

1
Anda mengatakan bahwa Anda tidak boleh menggunakan Promise.resolve()untuk membuat janji yang sudah terselesaikan dalam kehidupan nyata. Kenapa tidak? Promise.resolve()tampaknya lebih bersih dari new Promise(success => success()).
canac

8
@canac Maaf, itu hanya lelucon dengan permainan kata-kata ("janji kosong .."). Jelas digunakan Promise.resolve();dalam kode Anda.
Shridhar Gupta

1
Solusi yang bagus, mudah diikuti. Saya tidak menyertakan fungsi saya, jadi untuk menyelesaikan di akhir alih-alih menempatkan return sequence;saya meletakkansequence.then(() => { do stuff });
Joe Coyle

25

Pemanfaatan sederhana untuk janji Node.js standar:

function sequence(tasks, fn) {
    return tasks.reduce((promise, task) => promise.then(() => fn(task)), Promise.resolve());
}

MEMPERBARUI

item-janji adalah paket NPM siap digunakan melakukan hal yang sama.


6
Saya ingin melihat ini dijelaskan secara lebih rinci.
Tyguy7

Saya memberikan variasi jawaban ini dengan penjelasan di bawah ini. Terima kasih
Sarsaparilla

Inilah yang saya lakukan di lingkungan pra-Node 7 yang tidak memiliki akses ke async / menunggu. Bagus dan bersih.
JHH

11

Saya harus menjalankan banyak tugas berurutan dan menggunakan jawaban ini untuk membentuk fungsi yang akan menangani semua tugas berurutan ...

function one_by_one(objects_array, iterator, callback) {
    var start_promise = objects_array.reduce(function (prom, object) {
        return prom.then(function () {
            return iterator(object);
        });
    }, Promise.resolve()); // initial
    if(callback){
        start_promise.then(callback);
    }else{
        return start_promise;
    }
}

Fungsi ini mengambil 2 argumen + 1 opsional. Argumen pertama adalah array tempat kita akan bekerja. Argumen kedua adalah tugas itu sendiri, fungsi yang mengembalikan janji, tugas berikutnya akan dimulai hanya ketika janji ini terselesaikan. Argumen ketiga adalah panggilan balik untuk dijalankan ketika semua tugas telah dilakukan. Jika tidak ada panggilan balik yang dilewati, maka fungsi mengembalikan janji yang dibuatnya sehingga kami dapat menangani akhirnya.

Berikut ini contoh penggunaannya:

var filenames = ['1.jpg','2.jpg','3.jpg'];
var resize_task = function(filename){
    //return promise of async resizing with filename
};
one_by_one(filenames,resize_task );

Semoga ini menghemat waktu ...


Solusi yang luar biasa, ini adalah yang terbaik yang saya temukan dalam hampir seminggu stuggling .... Ini dijelaskan dengan sangat baik, memiliki nama-nama dalam yang logis, contoh yang baik (bisa lebih baik), saya dapat meminta itu dengan aman karena banyak sesuai kebutuhan, dan itu termasuk opsi untuk mengatur panggilan balik. cukup bagus! (Hanya mengubah nama menjadi sesuatu yang membuat saya lebih masuk akal) .... REKOMENDASI ​​untuk orang lain ... Anda dapat mengulangi suatu objek menggunakan 'Object.keys ( myObject )' sebagai 'object_array' Anda
DavidTaubmann

Terima kasih atas komentar Anda! Saya juga tidak menggunakan nama itu, tetapi saya ingin membuatnya lebih jelas / sederhana di sini.
Salketer

5

Solusi terbaik yang bisa saya temukan adalah dengan bluebirdjanji. Anda hanya bisa melakukan Promise.resolve(files).each(fs.readFileAsync);yang menjamin bahwa janji-janji diselesaikan secara berurutan.


1
Bahkan lebih baik: Promise.each(filtes, fs.readFileAsync). Btw, tidak harus Anda lakukan .bind(fs)?
Bergi 615

Tak seorang pun di sini tampaknya memahami perbedaan antara array dan urutan, bahwa yang terakhir menyiratkan ukuran tidak terbatas / dinamis.
vitaly-t

Perhatikan bahwa Array dalam Javascript tidak ada hubungannya dengan array ukuran tetap dalam bahasa gaya C. Mereka hanya objek dengan manajemen kunci numerik melesat, dan tidak memiliki ukuran atau batas yang ditentukan ( terutama tidak ketika menggunakan new Array(int). Semua yang dilakukan adalah preset lengthpasangan nilai kunci, yang mempengaruhi berapa banyak indeks yang digunakan selama iterasi berbasis panjang. Ia memiliki nol berpengaruh pada pengindeksan atau batas indeks array aktual)
Mike 'Pomax' Kamermans

4

Ini sedikit variasi dari jawaban lain di atas. Menggunakan Janji asli:

function inSequence(tasks) {
    return tasks.reduce((p, task) => p.then(task), Promise.resolve())
}

Penjelasan

Jika Anda memiliki tugas ini [t1, t2, t3], maka yang di atas setara denganPromise.resolve().then(t1).then(t2).then(t3) . Itu perilaku mengurangi.

Cara Penggunaan

Pertama, Anda perlu membuat daftar tugas! Tugas adalah fungsi yang tidak menerima argumen. Jika Anda harus meneruskan argumen ke fungsi Anda, gunakan bindatau metode lain untuk membuat tugas. Sebagai contoh:

var tasks = files.map(file => processFile.bind(null, file))
inSequence(tasks).then(...)

4

Solusi pilihan saya:

function processArray(arr, fn) {
    return arr.reduce(
        (p, v) => p.then((a) => fn(v).then(r => a.concat([r]))),
        Promise.resolve([])
    );
}

Ini tidak berbeda secara mendasar dari yang lain yang diterbitkan di sini tetapi:

  • Menerapkan fungsi ke item secara seri
  • Menyelesaikan berbagai hasil
  • Tidak memerlukan async / menunggu (dukungan masih sangat terbatas, sekitar 2017)
  • Menggunakan fungsi panah; bagus dan ringkas

Contoh penggunaan:

const numbers = [0, 4, 20, 100];
const multiplyBy3 = (x) => new Promise(res => res(x * 3));

// Prints [ 0, 12, 60, 300 ]
processArray(numbers, multiplyBy3).then(console.log);

Diuji pada Chrome saat ini yang wajar (v59) dan NodeJS (v8.1.2).


3

Gunakan Array.prototype.reduce, dan ingatlah untuk membungkus janji-janji Anda dalam suatu fungsi jika tidak maka janji itu sudah akan berjalan!

// array of Promise providers

const providers = [
  function(){
     return Promise.resolve(1);
  },
  function(){
     return Promise.resolve(2);
  },
  function(){
     return Promise.resolve(3);
  }
]


const inSeries = function(providers){

  const seed = Promise.resolve(null); 

  return providers.reduce(function(a,b){
      return a.then(b);
  }, seed);
};

bagus dan mudah ... Anda harus dapat menggunakan kembali benih yang sama untuk kinerja, dll.

Sangat penting untuk menjaga dari array kosong atau array dengan hanya 1 elemen saat menggunakan mengurangi , jadi teknik ini adalah taruhan terbaik Anda:

   const providers = [
      function(v){
         return Promise.resolve(v+1);
      },
      function(v){
         return Promise.resolve(v+2);
      },
      function(v){
         return Promise.resolve(v+3);
      }
    ]

    const inSeries = function(providers, initialVal){

        if(providers.length < 1){
            return Promise.resolve(null)
        }

        return providers.reduce((a,b) => a.then(b), providers.shift()(initialVal));
    };

dan kemudian menyebutnya seperti:

inSeries(providers, 1).then(v => {
   console.log(v);  // 7
});

2

Saya membuat metode sederhana ini pada objek Janji:

Buat dan tambahkan metode Promise.afterence ke objek Promise

Promise.sequence = function (chain) {
    var results = [];
    var entries = chain;
    if (entries.entries) entries = entries.entries();
    return new Promise(function (yes, no) {
        var next = function () {
            var entry = entries.next();
            if(entry.done) yes(results);
            else {
                results.push(entry.value[1]().then(next, function() { no(results); } ));
            }
        };
        next();
    });
};

Pemakaian:

var todo = [];

todo.push(firstPromise);
if (someCriterium) todo.push(optionalPromise);
todo.push(lastPromise);

// Invoking them
Promise.sequence(todo)
    .then(function(results) {}, function(results) {});

Hal terbaik tentang ekstensi ini ke objek Janji, adalah konsisten dengan gaya janji. Promise.all dan Promise.afterence dipanggil dengan cara yang sama, tetapi memiliki semantik yang berbeda.

Peringatan

Menjalankan janji secara berurutan biasanya bukan cara yang sangat baik untuk menggunakan janji. Biasanya lebih baik menggunakan Promise.all, dan biarkan browser menjalankan kode secepat mungkin. Namun, ada kasus penggunaan nyata untuk itu - misalnya saat menulis aplikasi seluler menggunakan javascript.


Tidak, Anda tidak dapat membandingkan Promise.alldan Anda Promise.sequence. Yang satu mengambil iterable dari janji, yang lain mengambil berbagai fungsi yang mengembalikan janji.
Bergi

Btw, saya akan merekomendasikan untuk menghindari antipattern constructor janji
Bergi

Tidak tahu bahwa butuh iterator. Seharusnya cukup mudah untuk menulis ulang. Bisakah Anda menjelaskan mengapa ini adalah antipattern konstruktor janji? Saya memang membaca posting Anda di sini: stackoverflow.com/a/25569299/1667011
frodeborli

@Bergi Saya telah memperbarui kode untuk mendukung iterator. Saya masih tidak melihat bahwa ini adalah antipattern. Antipatterns umumnya dianggap pedoman untuk menghindari kesalahan pengkodean, dan itu benar-benar berlaku untuk membuat (perpustakaan) fungsi yang melanggar pedoman itu.
frodeborli

Ya, jika Anda menganggapnya sebagai fungsi perpustakaan tidak apa-apa, tetapi masih dalam kasus reduceini jawaban seperti pada Benjamin jauh lebih sederhana.
Bergi

2

Anda dapat menggunakan fungsi ini yang mendapat Daftar janjiPabrik:

function executeSequentially(promiseFactories) {
    var result = Promise.resolve();
    promiseFactories.forEach(function (promiseFactory) {
        result = result.then(promiseFactory);
    });
    return result;
}

Pabrik Janji hanyalah fungsi sederhana yang mengembalikan Janji:

function myPromiseFactory() {
    return somethingThatCreatesAPromise();
}

Ini bekerja karena pabrik janji tidak menciptakan janji sampai diminta. Ini bekerja dengan cara yang sama seperti fungsi saat itu - pada kenyataannya, itu adalah hal yang sama!

Anda sama sekali tidak ingin beroperasi di atas berbagai janji. Sesuai spesifikasi Janji, segera setelah janji dibuat, janji mulai dijalankan. Jadi yang benar-benar Anda inginkan adalah berbagai pabrik janji ...

Jika Anda ingin mempelajari lebih lanjut tentang Janji, Anda harus memeriksa tautan ini: https://pouchdb.com/2015/05/18/we-have-a-problem-with-promises.html


2

Jawaban saya berdasarkan https://stackoverflow.com/a/31070150/7542429 .

Promise.series = function series(arrayOfPromises) {
    var results = [];
    return arrayOfPromises.reduce(function(seriesPromise, promise) {
      return seriesPromise.then(function() {
        return promise
        .then(function(result) {
          results.push(result);
        });
      });
    }, Promise.resolve())
    .then(function() {
      return results;
    });
  };

Solusi ini mengembalikan hasilnya sebagai array seperti Promise.all ().

Pemakaian:

Promise.series([array of promises])
.then(function(results) { 
  // do stuff with results here
});

2

Saya benar-benar menyukai jawaban @ joelnet, tetapi bagi saya, gaya pengkodean itu agak sulit dicerna, jadi saya menghabiskan beberapa hari mencoba mencari tahu bagaimana saya akan mengekspresikan solusi yang sama dengan cara yang lebih mudah dibaca dan ini adalah saya ambil, hanya dengan sintaks yang berbeda dan beberapa komentar.

// first take your work
const urls = ['/url1', '/url2', '/url3', '/url4']

// next convert each item to a function that returns a promise
const functions = urls.map((url) => {
  // For every url we return a new function
  return () => {
    return new Promise((resolve) => {
      // random wait in milliseconds
      const randomWait = parseInt((Math.random() * 1000),10)
      console.log('waiting to resolve in ms', randomWait)
      setTimeout(()=>resolve({randomWait, url}),randomWait)
    })
  }
})


const promiseReduce = (acc, next) => {
  // we wait for the accumulator to resolve it's promise
  return acc.then((accResult) => {
    // and then we return a new promise that will become
    // the new value for the accumulator
    return next().then((nextResult) => {
      // that eventually will resolve to a new array containing
      // the value of the two promises
      return accResult.concat(nextResult)
    })
  })
};
// the accumulator will always be a promise that resolves to an array
const accumulator = Promise.resolve([])

// we call reduce with the reduce function and the accumulator initial value
functions.reduce(promiseReduce, accumulator)
  .then((result) => {
    // let's display the final value here
    console.log('=== The final result ===')
    console.log(result)
  })

2

Seperti yang diperhatikan Bergi, saya pikir solusi terbaik dan jelas adalah menggunakan BlueBird.each, kode di bawah ini:

const BlueBird = require('bluebird');
BlueBird.each(files, fs.readFileAsync);

2

Pertama, Anda perlu memahami bahwa janji dijalankan pada saat penciptaan.
Jadi misalnya jika Anda memiliki kode:

["a","b","c"].map(x => returnsPromise(x))

Anda perlu mengubahnya ke:

["a","b","c"].map(x => () => returnsPromise(x))

Maka kita perlu secara berantai janji-janji:

["a", "b", "c"].map(x => () => returnsPromise(x))
    .reduce(
        (before, after) => before.then(_ => after()),
        Promise.resolve()
    )

mengeksekusi after(), akan memastikan bahwa janji dibuat (dan dieksekusi) hanya ketika waktunya tiba.


1

Saya menggunakan kode berikut untuk memperluas objek Janji. Ini menangani penolakan janji-janji dan mengembalikan berbagai hasil

Kode

/*
    Runs tasks in sequence and resolves a promise upon finish

    tasks: an array of functions that return a promise upon call.
    parameters: an array of arrays corresponding to the parameters to be passed on each function call.
    context: Object to use as context to call each function. (The 'this' keyword that may be used inside the function definition)
*/
Promise.sequence = function(tasks, parameters = [], context = null) {
    return new Promise((resolve, reject)=>{

        var nextTask = tasks.splice(0,1)[0].apply(context, parameters[0]); //Dequeue and call the first task
        var output = new Array(tasks.length + 1);
        var errorFlag = false;

        tasks.forEach((task, index) => {
            nextTask = nextTask.then(r => {
                output[index] = r;
                return task.apply(context, parameters[index+1]);
            }, e=>{
                output[index] = e;
                errorFlag = true;
                return task.apply(context, parameters[index+1]);
            });
        });

        // Last task
        nextTask.then(r=>{
            output[output.length - 1] = r;
            if (errorFlag) reject(output); else resolve(output);
        })
        .catch(e=>{
            output[output.length - 1] = e;
            reject(output);
        });
    });
};

Contoh

function functionThatReturnsAPromise(n) {
    return new Promise((resolve, reject)=>{
        //Emulating real life delays, like a web request
        setTimeout(()=>{
            resolve(n);
        }, 1000);
    });
}

var arrayOfArguments = [['a'],['b'],['c'],['d']];
var arrayOfFunctions = (new Array(4)).fill(functionThatReturnsAPromise);


Promise.sequence(arrayOfFunctions, arrayOfArguments)
.then(console.log)
.catch(console.error);

1

Jika mau, Anda dapat menggunakan mengurangi untuk membuat janji berurutan, misalnya:

[2,3,4,5,6,7,8,9].reduce((promises, page) => {
    return promises.then((page) => {
        console.log(page);
        return Promise.resolve(page+1);
    });
  }, Promise.resolve(1));

itu akan selalu bekerja secara berurutan.


1

Menggunakan ES modern:

const series = async (tasks) => {
  const results = [];

  for (const task of tasks) {
    const result = await task;

    results.push(result);
  }

  return results;
};

//...

const readFiles = await series(files.map(readFile));

1

Dengan Async / Menunggu (jika Anda mendapat dukungan ES7)

function downloadFile(fileUrl) { ... } // This function return a Promise

async function main()
{
  var filesList = [...];

  for (const file of filesList) {
    await downloadFile(file);
  }
}

(Anda harus menggunakan forlingkaran, dan tidakforEach karena async / menunggu memiliki masalah berjalan di forEach loop)

Tanpa Async / Menunggu (menggunakan Janji)

function downloadFile(fileUrl) { ... } // This function return a Promise

function downloadRecursion(filesList, index)
{
  index = index || 0;
  if (index < filesList.length)
  {
    downloadFile(filesList[index]).then(function()
    {
      index++;
      downloadRecursion(filesList, index); // self invocation - recursion!
    });
  }
  else
  {
    return Promise.resolve();
  }
}

function main()
{
  var filesList = [...];
  downloadRecursion(filesList);
}

2
Menunggu di dalam forEach tidak dianjurkan.
Marcelo Agimóvel

@ MarceloAgimóvel - Saya telah memperbarui solusi untuk tidak bekerja dengan forEach(sesuai dengan ini )
Gil Epshtain

0

Atas dasar judul pertanyaan, "Selesaikan satu demi satu janji (yaitu secara berurutan)?", Kita mungkin memahami bahwa OP lebih tertarik pada penanganan berurutan janji pada penyelesaian daripada panggilan berurutan per se .

Jawaban ini ditawarkan:

  • untuk menunjukkan bahwa panggilan berurutan tidak diperlukan untuk penanganan respons berurutan.
  • untuk mengekspos pola alternatif yang layak untuk pengunjung halaman ini - termasuk OP jika dia masih tertarik lebih dari setahun kemudian.
  • terlepas dari pernyataan OP bahwa dia tidak ingin melakukan panggilan secara bersamaan, yang mungkin benar-benar terjadi tetapi sama-sama dapat menjadi asumsi berdasarkan keinginan untuk penanganan tanggapan berurutan sesuai dengan judulnya.

Jika panggilan bersamaan benar-benar tidak diinginkan maka lihat jawaban Benjamin Gruenbaum yang mencakup panggilan berurutan (dll) secara komprehensif.

Namun, jika Anda tertarik (untuk peningkatan kinerja) dalam pola yang memungkinkan panggilan serentak diikuti dengan penanganan respons berurutan, maka silakan baca terus.

Sangat menggoda untuk berpikir Anda harus menggunakan Promise.all(arr.map(fn)).then(fn)(seperti yang telah saya lakukan berkali-kali) atau gula mewah lib Promise (terutama Bluebird), namun (dengan kredit untuk artikel ini ) sebuah arr.map(fn).reduce(fn)pola akan melakukan pekerjaan, dengan keuntungan bahwa:

  • bekerja dengan sembarang janji - bahkan versi jQuery yang sesuai sebelumnya - hanya .then() digunakan.
  • memberikan fleksibilitas untuk melewatkan kesalahan atau menghentikan kesalahan, mana pun yang Anda inginkan dengan mod satu baris.

Ini dia, ditulis untuk Q.

var readFiles = function(files) {
    return files.map(readFile) //Make calls in parallel.
    .reduce(function(sequence, filePromise) {
        return sequence.then(function() {
            return filePromise;
        }).then(function(file) {
            //Do stuff with file ... in the correct sequence!
        }, function(error) {
            console.log(error); //optional
            return sequence;//skip-over-error. To stop-on-error, `return error` (jQuery), or `throw  error` (Promises/A+).
        });
    }, Q()).then(function() {
        // all done.
    });
};

Catatan: hanya satu fragmen itu, Q() yang khusus untuk Q. Untuk jQuery Anda harus memastikan bahwa readFile () mengembalikan janji jQuery. Dengan A + libs, janji-janji asing akan berasimilasi.

Kuncinya di sini adalah pengurangan ini sequencejanji, yang sekuens yang penanganan darireadFile janji janji tetapi bukan penciptaannya.

Dan begitu Anda menyerapnya, mungkin sedikit mengejutkan ketika Anda menyadari bahwa .map()panggung sebenarnya tidak diperlukan! Seluruh pekerjaan, panggilan paralel ditambah penanganan serial dalam urutan yang benar, dapat dicapai dengan reduce()sendirian, ditambah keuntungan tambahan dari fleksibilitas lebih lanjut untuk:

  • mengkonversi dari panggilan async paralel ke panggilan async serial dengan hanya memindahkan satu baris - berpotensi berguna selama pengembangan.

Ini dia, untuk Qlagi.

var readFiles = function(files) {
    return files.reduce(function(sequence, f) {
        var filePromise = readFile(f);//Make calls in parallel. To call sequentially, move this line down one.
        return sequence.then(function() {
            return filePromise;
        }).then(function(file) {
            //Do stuff with file ... in the correct sequence!
        }, function(error) {
            console.log(error); //optional
            return sequence;//Skip over any errors. To stop-on-error, `return error` (jQuery), or `throw  error` (Promises/A+).
        });
    }, Q()).then(function() {
        // all done.
    });
};

Itulah pola dasarnya. Jika Anda juga ingin mengirimkan data (misalnya file atau beberapa transformasi) ke pemanggil, Anda memerlukan varian yang ringan.


Saya tidak berpikir itu ide yang baik untuk menjawab pertanyaan yang bertentangan dengan niat OPs ...
Bergi 9'15

1
Hal ini sequence.then(() => filePromise)adalah antipattern - ia tidak menyebarkan kesalahan secepat mungkin (dan membuat unhandledRejectionlib yang mendukungnya). Anda lebih baik menggunakan Q.all([sequence, filePromise])atau $.when(sequence, filePromise). Memang, perilaku ini mungkin yang Anda inginkan ketika Anda bertujuan untuk mengabaikan atau melewatkan kesalahan, tetapi Anda setidaknya harus menyebutkan ini sebagai kerugian.
Bergi

@Bergi, saya berharap OP akan turun tangan dan memberikan penilaian apakah ini benar-benar bertentangan dengan niatnya atau tidak. Jika tidak, saya akan menghapus jawaban yang saya kira, sementara itu saya harap saya membenarkan posisi saya. Terima kasih telah menganggapnya cukup serius untuk memberikan umpan balik yang layak. Bisakah Anda menjelaskan lebih lanjut tentang pola anti, atau tolong berikan referensi? Apakah hal yang sama berlaku untuk artikel tempat saya menemukan pola dasar ?
Roamer-1888

1
Ya, versi ketiga kodenya (yaitu "paralel dan berurutan") memiliki masalah yang sama. "Antipattern" membutuhkan penanganan kesalahan yang canggih dan cenderung untuk melampirkan penangan secara tidak sinkron, yang menyebabkan unhandledRejectionperistiwa. Di Bluebird Anda bisa mengatasi ini dengan menggunakan sequence.return(filePromise)yang memiliki perilaku yang sama tetapi menangani penolakan dengan baik. Saya tidak tahu referensi apa pun, saya baru saja memunculkannya - saya tidak berpikir "pola (anti)" belum memiliki nama.
Bergi

1
@Bergi, Anda dapat dengan jelas melihat sesuatu yang tidak dapat saya :( Saya ingin tahu apakah pola anti-baru ini perlu didokumentasikan di suatu tempat?
Roamer-1888

0

Pendekatan Anda tidak buruk, tetapi memang memiliki dua masalah: ia menelan kesalahan dan mempekerjakan Antipattern Konstruksi Eksplisit Janji.

Anda dapat menyelesaikan kedua masalah ini, dan membuat kode lebih bersih, sambil tetap menggunakan strategi umum yang sama:

var Q = require("q");

var readFile = function(file) {
  ... // Returns a promise.
};

var readFiles = function(files) {
  var readSequential = function(index) {
    if (index < files.length) {
      return readFile(files[index]).then(function() {
        return readSequential(index + 1);
      });
    }
  };

  // using Promise.resolve() here in case files.length is 0
  return Promise.resolve(readSequential(0)); // Start!
};

0

Jika orang lain membutuhkan cara yang dijamin secara KUAT untuk menyelesaikan Janji saat melakukan operasi CRUD, Anda juga dapat menggunakan kode berikut sebagai dasar.

Selama Anda menambahkan 'kembali' sebelum memanggil setiap fungsi, mendeskripsikan Janji, dan menggunakan contoh ini sebagai dasar pemanggilan fungsi .then () selanjutnya secara KONSISTEN akan mulai setelah selesainya yang sebelumnya:

getRidOfOlderShoutsPromise = () => {
    return readShoutsPromise('BEFORE')
    .then(() => {
        return deleteOlderShoutsPromise();
    })
    .then(() => {
        return readShoutsPromise('AFTER')
    })
    .catch(err => console.log(err.message));
}

deleteOlderShoutsPromise = () => {
    return new Promise ( (resolve, reject) => {
        console.log("in deleteOlderShouts");
        let d = new Date();
        let TwoMinuteAgo = d - 1000 * 90 ;
        All_Shouts.deleteMany({ dateTime: {$lt: TwoMinuteAgo}}, function(err) {
            if (err) reject();
            console.log("DELETED OLDs at "+d);
            resolve();        
        });
    });
}

readShoutsPromise = (tex) => {
    return new Promise( (resolve, reject) => {
        console.log("in readShoutsPromise -"+tex);
        All_Shouts
        .find({})
        .sort([['dateTime', 'ascending']])
        .exec(function (err, data){
            if (err) reject();
            let d = new Date();
            console.log("shouts "+tex+" delete PROMISE = "+data.length +"; date ="+d);
            resolve(data);
        });    
    });
}

0

Metode push dan pop array dapat digunakan untuk urutan janji. Anda juga dapat mendorong janji baru saat Anda membutuhkan data tambahan. Ini adalah kode yang akan saya gunakan di React Infinite loader untuk memuat urutan halaman.

var promises = [Promise.resolve()];

function methodThatReturnsAPromise(page) {
	return new Promise((resolve, reject) => {
		setTimeout(() => {
			console.log(`Resolve-${page}! ${new Date()} `);
			resolve();
		}, 1000);
	});
}

function pushPromise(page) {
	promises.push(promises.pop().then(function () {
		return methodThatReturnsAPromise(page)
	}));
}

pushPromise(1);
pushPromise(2);
pushPromise(3);


0

Sebagian besar jawaban tidak menyertakan hasil SEMUA janji secara individual, jadi jika seseorang mencari perilaku khusus ini, ini adalah solusi yang memungkinkan menggunakan rekursi.

Ini mengikuti gaya Promise.all:

  • Mengembalikan array hasil dalam .then()panggilan balik.

  • Jika beberapa janji gagal, segera dikembalikan dalam .catch()panggilan balik.

const promiseEach = (arrayOfTasks) => {
  let results = []
  return new Promise((resolve, reject) => {
    const resolveNext = (arrayOfTasks) => {
      // If all tasks are already resolved, return the final array of results
      if (arrayOfTasks.length === 0) return resolve(results)

      // Extract first promise and solve it
      const first = arrayOfTasks.shift()

      first().then((res) => {
        results.push(res)
        resolveNext(arrayOfTasks)
      }).catch((err) => {
        reject(err)
      })
    }
    resolveNext(arrayOfTasks)
  })
}

// Lets try it 😎

const promise = (time, shouldThrowError) => new Promise((resolve, reject) => {
  const timeInMs = time * 1000
  setTimeout(()=>{
    console.log(`Waited ${time} secs`)
    if (shouldThrowError) reject(new Error('Promise failed'))
    resolve(time)
  }, timeInMs)
})

const tasks = [() => promise(1), () => promise(2)]

promiseEach(tasks)
  .then((res) => {
    console.log(res) // [1, 2]
  })
  // Oops some promise failed
  .catch((error) => {
    console.log(error)
  })

Perhatikan tentang tasksdeklarasi array :

Dalam hal ini tidak mungkin untuk menggunakan notasi berikut seperti yang Promise.allakan digunakan:

const tasks = [promise(1), promise(2)]

Dan kita harus menggunakan:

const tasks = [() => promise(1), () => promise(2)]

Alasannya adalah bahwa JavaScript mulai menjalankan janji segera setelah diumumkan. Jika kita menggunakan metode seperti Promise.all, itu hanya memeriksa bahwa semua dari mereka adalah fulfilledatau rejected, tetapi tidak memulai exection itu sendiri. Menggunakan () => promise()kami menghentikan eksekusi sampai dipanggil.


0
(function() {
  function sleep(ms) {
    return new Promise(function(resolve) {
      setTimeout(function() {
        return resolve();
      }, ms);
    });
  }

  function serial(arr, index, results) {
    if (index == arr.length) {
      return Promise.resolve(results);
    }
    return new Promise(function(resolve, reject) {
      if (!index) {
        index = 0;
        results = [];
      }
      return arr[index]()
        .then(function(d) {
          return resolve(d);
        })
        .catch(function(err) {
          return reject(err);
        });
    })
      .then(function(result) {
        console.log("here");
        results.push(result);
        return serial(arr, index + 1, results);
      })
      .catch(function(err) {
        throw err;
      });
  }

  const a = [5000, 5000, 5000];

  serial(a.map(x => () => sleep(x)));
})();

Di sini kuncinya adalah bagaimana Anda memanggil fungsi tidur. Anda harus melewati berbagai fungsi yang dengan sendirinya mengembalikan janji alih-alih array janji.


-1

Ini untuk memperluas tentang bagaimana memproses urutan janji dengan cara yang lebih umum, mendukung urutan dinamis / tak terbatas, berdasarkan pada implementasi spex.

var $q = require("q");
var spex = require('spex')($q);

var files = []; // any dynamic source of files;

var readFile = function (file) {
    // returns a promise;
};

function source(index) {
    if (index < files.length) {
        return readFile(files[index]);
    }
}

function dest(index, data) {
    // data = resolved data from readFile;
}

spex.sequence(source, dest)
    .then(function (data) {
        // finished the sequence;
    })
    .catch(function (error) {
        // error;
    });

Tidak hanya solusi ini akan bekerja dengan urutan ukuran apa pun, tetapi Anda dapat dengan mudah menambahkan pelambatan data dan load balancing ke dalamnya.

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.