Вирішіть обіцянки одна за одною (тобто послідовно)?


269

Розглянемо наступний код, який читає масив файлів послідовно / послідовно. readFilesповертає обіцянку, яка вирішується лише після того, як усі файли будуть прочитані послідовно.

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!

  });
};

Наведений вище код працює, але мені не подобається робити рекурсії для того, щоб справи відбувалися послідовно. Чи є простіший спосіб переписати цей код, щоб мені не довелося використовувати свою дивну readSequentialфункцію?

Спочатку я намагався використовувати Promise.all, але це викликало те, що всі readFileдзвінки відбуваються одночасно, а це не те, що я хочу:

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

2
Все, що чекатиме завершення попередньої асинхронної операції, повинно бути виконано у зворотній зв'язок. Використання обіцянок цього не змінює. Тож вам потрібна рекурсія.
Бармар

1
FYI, це технічно не є рекурсією, оскільки немає складання кадру стека. Попередній readFileSequential()вже повернувся до виклику наступного (оскільки це асинхронізація, він завершується довго після того, як вихідний функціональний виклик вже повернувся).
jfriend00

1
@ jfriend00 Накопичення кадру стека для рекурсії не потрібно - лише самовідвідання. Це лише технічність.
Бенджамін Грюенбаум

3
@BenjaminGruenbaum - моя думка в тому, що немає нічого поганого в тому, щоб функція викликала себе, щоб розпочати наступну ітерацію. На це немає нуля, і насправді це ефективний спосіб послідовності асинхронних операцій. Отже, немає причин уникати чогось, схожого на рекурсію. Існують рекурсивні рішення деяких неефективних проблем - це не одна з таких.
jfriend00

1
Привіт, на дискусію та запит у кімнаті JavaScript я відредагував цю відповідь, щоб ми могли вказати на неї як на канонічну. Якщо ви не згодні, будь ласка, повідомте мене, і я відновлю його та відкрию окремий.
Бенджамін Грюнбаум

Відповіді:


337

Оновлення 2017 року : я б використовував функцію асинхронізації, якщо середовище її підтримує:

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

Якщо ви хочете, ви можете відкласти читання файлів, поки вони не знадобляться за допомогою генератора асинхронізації (якщо ваше середовище це підтримує):

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

Оновлення: По-друге, я можу замість цього використати цикл:

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

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

Або, більш компактно, зі зменшенням:

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

В інших бібліотеках з обіцянками (наприклад, коли і Bluebird) у вас є корисні методи для цього.

Наприклад, Bluebird буде:

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

Хоча насправді немає причин не використовувати асинхронізацію, чекаємо сьогодні.


2
@ EmreTapcı, nope. Функція стрілки "=>" вже передбачає повернення.
Макс

Якщо ви використовуєте TypeScript, я думаю, що рішення для циклу "for in" найкраще. Зменшення віддачі рекурсивних обіцянок, наприклад. перший тип повернення дзвінка - Promise <void>, потім другий - Promise <Promise <void>> і так далі - неможливо набрати без використання будь-якого, я думаю
Артур Тагісов

@ArturTagisow TypeScript (принаймні нові версії) мають рекурсивні типи і повинні правильно вирішувати типи тут. Не існує такого поняття, як Обіцянка <Обіцянка <T>>, оскільки обіцянки "рекурсивно асимілюються". Promise.resolve(Promise.resolve(15))тотожна Promise.resolve(15).
Бенджамін Груенбаум


72

Ось як я волію виконувати завдання послідовно.

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

Що щодо справ із більшою кількістю завдань? Мовляв, 10?

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

8
А як щодо випадків, коли ви не знаєте точної кількості завдань?
проклятий

1
А як бути, коли ви знаєте кількість завдань, але лише під час виконання?
joeytwiddle

10
"Ви взагалі не хочете оперувати масивом обіцянок. За специфікацією обіцянки, як тільки обіцянка створена, вона починає виконуватись. Отже, що ви дійсно хочете, це масив фабрик обіцянок" див. Розширена помилка №3 тут: pouchdb.com/2015/05/18/we-have-a-problem-with-promises.html
edelans

5
Якщо ви зменшите шум лінії, ви також можете написатиresult = result.then(task);
Даніель Бакмастер

1
@DanielBuckmaster так, але будьте обережні, оскільки якщо task () повертає значення, воно буде передане наступному виклику. Якщо у вашому завданні є необов'язкові аргументи, це може спричинити побічні ефекти. Поточний код проковтує результати і явно викликає наступне завдання без аргументів.
JHH

63

Це питання давнє, але ми живемо у світі ES6 та функціонального JavaScript, тож давайте подивимось, як ми можемо покращити.

Оскільки обіцянки виконуються негайно, ми не можемо просто створити масив обіцянок, вони все б увімкнули паралельно.

Натомість нам потрібно створити масив функцій, який повертає обіцянку. Кожна функція буде виконуватися послідовно, що потім запускає обіцянку всередині.

Ми можемо вирішити це кількома способами, але мій улюблений спосіб - це використовувати reduce .

Це стає трохи хитро, використовуючи reduceв поєднанні з обіцянками, тому я розбив один лайнер на кілька менших засвоюваних укусів нижче.

Суть цієї функції полягає у використанні reduceпочинаючи з початкового значення Promise.resolve([])або обіцянки, що містить порожній масив.

Потім ця обіцянка буде передана в reduceметод як promise. Це ключ до того, щоб послідовно поєднувати кожну обіцянку. Наступна обіцянка, яку потрібно виконати, - funcі коли thenпожежі, результати об'єднуються, і ця обіцянка потім повертається, виконуючи reduceцикл із наступною функцією обіцянки.

Після виконання всіх обіцянок повернута обіцянка буде містити масив усіх результатів кожної обіцянки.

Приклад ES6 (один вкладиш)

/*
 * 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([]))

Приклад ES6 (розбитий)

// 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([]))

Використання:

// 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
дуже добре, спасибі, Array.prototype.concat.bind(result)це частина, якої мені не вистачало, довелося підштовхувати до результатів вручну, яка працювала, але була менш крутою
завершення

Оскільки ми всі про сучасний JS, я вважаю, що console.log.bind(console)твердження у вашому останньому прикладі зараз зазвичай непотрібне. У ці дні можна просто пройти console.log. Напр. serial(funcs).then(console.log). Тестовано на поточних вузлах та Chrome.
Моломбі

Це було трохи важко обернути мою голову, але скорочення по суті робить це правильно? 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, так, це виглядає правильно. Ви також можете скинути Promise.resolve у відповідь, будь-які повернуті значення будуть автоматично вирішені, якщо ви не зателефонуєте на них Promise.reject.
joelnet

@joelnet, у відповідь на коментар danecando, я думаю, що зниження має бути правильнішим виразом у наступному виразі, ви згодні? Promise.resolve([]).then(x => someApiCall('url1').then(r => x.concat(r))).then(x => someApiCall('url2').then(r => x.concat(r)))і так далі
bufferoverflow76

37

Для цього просто в 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
Здається, це використовується підкреслення. Ви можете спростити, files.forEachякщо файли є масивом.
Gustavo Rodrigues

2
Ну ... це ES5. Шлях у ES6 був би for (file of files) {...}.
Gustavo Rodrigues

1
Ви кажете, що не слід використовувати Promise.resolve()для створення вже вирішеної обіцянки в реальному житті. Чому ні? Promise.resolve()здається чистішим, ніж new Promise(success => success()).
canac

8
@canac Вибачте, це був просто жарт із грою на слова ("порожні обіцянки .."). Виразно використовуйте Promise.resolve();у своєму коді.
Шрідхар Гупта

1
Приємне рішення, просте дотримання. Я не вкладав шахту в функцію, тому для вирішення в кінці замість того, щоб ставити, return sequence;я поставивsequence.then(() => { do stuff });
Джо Койл

25

Простий утиліти для стандартних Node.js обіцяють:

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

ОНОВЛЕННЯ

items-обіцянка - це готовий до використання NPM пакет, що робить те саме.


6
Я хотів би, щоб це було пояснено детальніше.
Tyguy7

Я надав варіацію цієї відповіді з поясненням нижче. Спасибі
Сарсапарілла

Це саме те, що я роблю в середовищах перед Node 7, не маючи доступу до асинхронізації / очікування. Приємно і чисто.
JHH

11

Мені довелося виконати багато послідовних завдань і використав ці відповіді, щоб сформувати функцію, яка б потурбувалась про обробку будь-яких послідовних завдань ...

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

Функція бере 2 аргументи + 1 необов'язково. Перший аргумент - це масив, над яким ми будемо працювати. Другий аргумент - це саме завдання, функція, яка повертає обіцянку, наступне завдання буде запущено лише тоді, коли ця обіцянка буде вирішена. Третій аргумент - це зворотний виклик, який потрібно запустити, коли всі завдання виконані. Якщо зворотний виклик не передано, функція повертає створену обіцянку, щоб ми могли обробити кінець.

Ось приклад використання:

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

Сподіваюсь, це заощадить когось деякий час ...


Неймовірне рішення, це було найкраще, що я знайшов протягом майже тижня приголомшливого .... Це дуже добре пояснено, має логічні внутрішні імена, хороший приклад (міг би бути кращим), я можу сміливо закликати до цього разів, коли це потрібно, і вона включає можливість встановлення зворотних дзвінків. просто НІКОЛИ! (Тільки змінив назву на те , що робить мене більше сенсу) .... Рекомендація для інших ... Ви можете перебирати об'єкт , використовуючи «Object.keys ( MyObject )» , як ваш «objects_array»
DavidTaubmann

Дякуємо за Ваш коментар! Я не використовую цього імені, але тут я хотів зробити його більш очевидним / простим.
Салькетер

5

Найприємніше рішення, яке мені вдалося зрозуміти, було bluebirdобіцянками. Ви можете просто зробити те, Promise.resolve(files).each(fs.readFileAsync);що гарантує, що обіцянки будуть вирішені послідовно в порядку.


1
Ще краще: Promise.each(filtes, fs.readFileAsync). Btw, чи не потрібно робити .bind(fs)?
Бергі

Здається, ніхто тут не розуміє різницю між масивом і послідовністю, що останній передбачає необмежений / динамічний розмір.
vitaly-t

Зауважте, що масиви в JavaScript не мають нічого спільного з масивами фіксованого розміру на мовах стилю C. Вони є лише об'єктами, на яких зафіксовано цифрове управління ключами, і не мають встановленого розміру чи обмеження ( особливо не під час використання new Array(int). Все, що робиться, це попередньо встановити lengthпару ключових значень, що впливає на те, скільки індексів використовується під час ітерації на основі довжини. Вона має нуль вплив на індексацію реального масиву або межі індексу)
Майк 'Pomax' Камерманс

4

Це невелика зміна іншої відповіді вище. Використання власних обіцянок:

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

Пояснення

Якщо у вас є ці завдання [t1, t2, t3], то вищезазначене рівносильно Promise.resolve().then(t1).then(t2).then(t3). Це поведінка скорочення.

Як використовувати

Спочатку вам потрібно побудувати перелік завдань! Завдання - це функція, яка не приймає аргументів. Якщо вам потрібно передати аргументи до вашої функції, тоді використовуйте bindабо інші методи для створення завдання. Наприклад:

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

4

Моє бажане рішення:

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

Він принципово не відрізняється від інших, опублікованих тут, але:

  • Застосовує функцію до елементів послідовно
  • Вирішує масив результатів
  • Не вимагає асинхронізації / очікування (підтримка ще досить обмежена, близько 2017 року)
  • Використовує функції стрілок; приємно і лаконічно

Приклад використання:

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

Тестовано на розумних поточних Chrome (v59) та NodeJS (v8.1.2).


3

Використовуйте Array.prototype.reduceта пам'ятайте, щоб зафіксувати свої обіцянки у функції, інакше вони вже будуть працювати!

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

приємно і просто ... ви повинні мати можливість повторно використовувати те саме насіння для виконання тощо.

Важливо захищати від порожніх масивів або масивів лише з 1 елементом при використанні скорочення , тому ця методика - найкраща ставка:

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

а потім називати це так:

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

2

Я створив цей простий метод на об’єкті Promise:

Створіть та додайте метод Promise.sequence до об’єкту 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();
    });
};

Використання:

var todo = [];

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

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

Найкраще в цьому розширенні на об’єкт Обіцяння - це те, що воно відповідає стилю обіцянок. Promise.all і Promise.sequence викликаються однаково, але мають різну семантику.

Обережність

Послідовне виконання обіцянок зазвичай не дуже вдалий спосіб використання обіцянок. Зазвичай краще використовувати Promise.all, і дозволити браузеру виконувати код якомога швидше. Однак для цього є реальні випадки використання - наприклад, під час написання мобільного додатку за допомогою JavaScript.


Ні, ви не можете порівняти Promise.allі ваше Promise.sequence. Один приймає ітерабельні обіцянки, інший бере масив функцій, які повертають обіцянки.
Бергі

Доречі, я б рекомендував уникати конструктора обіцянок антипатерн
Бергі

Не знав, що потрібен ітератор. Потрібно бути досить простим, щоб переписати його. Не могли б ви пояснити, чому це конструктор обіцянок антипатерн? Я прочитав ваш пост тут: stackoverflow.com/a/25569299/1667011
frodeborli

@Bergi Я оновив код, щоб підтримувати ітератори. Я досі не бачу, що це антипатерн. Антипакетів, як правило, слід вважати настановами, щоб уникнути помилок кодування, і цілком справедливо створювати (бібліотеку) функції, що порушують ці вказівки.
frodeborli

Так, якщо ви вважаєте, що це функція бібліотеки, це нормально, але все ж у цьому випадку reduceподібне у відповіді Бенджаміна просто набагато простіше.
Бергі

2

Ви можете скористатися цією функцією, яка отримує обіцяний Список фабрик:

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

Promise Factory - це просто проста функція, яка повертає обіцянку:

function myPromiseFactory() {
    return somethingThatCreatesAPromise();
}

Це працює, тому що фабрика обіцянок не створює обіцянки, поки її не попросять. Це працює так само, як і тодішня функція - насправді це те саме!

Ви взагалі не хочете оперувати масивом обіцянок. За специфікацією Promise, як тільки обіцянка буде створена, вона починає виконуватися. Тож те, що ти справді хочеш, - це масив фабрик із обіцянками ...

Якщо ви хочете дізнатися більше про Обіцяння, перевірте це посилання: https://pouchdb.com/2015/05/18/we-have-a-problem-with-promises.html


2

Моя відповідь на основі 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;
    });
  };

Це рішення повертає результати у вигляді масиву, як Promise.all ().

Використання:

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

2

Мені дуже сподобалась відповідь @ joelnet, але мені такий стиль кодування трохи важко засвоюється, тому я витратив пару днів на те, щоб зрозуміти, як би я висловив те саме рішення більш читабельним чином, і це мій візьміть, просто з іншим синтаксисом і деякими коментарями.

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

Як зауважив Бергі, я вважаю, що найкращим і зрозумілим рішенням є використання BlueBird.each, код нижче:

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

2

Спочатку потрібно зрозуміти, що обіцянка виконується під час створення.
Так, наприклад, якщо у вас є код:

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

Потрібно змінити його на:

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

Тоді нам потрібно послідовно ланцюжок обіцянок:

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

виконавши after(), переконається, що обіцянка буде створена (і виконана) лише тоді, коли прийде її час.


1

Я використовую наступний код для розширення об'єкта Promise. Він обробляє відмову від обіцянок і повертає масив результатів

Код

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

Приклад

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

Якщо ви хочете, ви можете скористатися скороченням, щоб зробити послідовну обіцянку, наприклад:

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

вона завжди працює в послідовному.


1

Використання сучасних ЕС:

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

За допомогою програми Async / Await (якщо у вас є підтримка ES7)

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

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

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

(ви повинні використовувати forцикл, а неforEach тому, що у async / await є проблеми із запуском у циклі forEach)

Без асинхронізації / очікування (з використанням обіцянки)

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
Чекати всередині для кожного не рекомендується.
Марсело Агімовель

@ MarceloAgimóvel - Я оновив рішення, щоб не працювати forEach(відповідно до цього )
Gil Epshtain

0

Виходячи з заголовка питання, "Вирішувати обіцянки одна за одною (тобто послідовно)?", Ми можемо зрозуміти, що ОП більше зацікавлена ​​в послідовній обробці обіцянок щодо врегулювання, ніж послідовних дзвінках як такої .

Ця відповідь пропонується:

  • щоб продемонструвати, що послідовні дзвінки не потрібні для послідовного опрацювання відповідей.
  • розкрити для відвідувачів цієї сторінки життєздатні альтернативні зразки, включаючи ОП, якщо він все ще зацікавився через рік.
  • незважаючи на твердження ОП, що він не хоче одночасно телефонувати на дзвінки, що справді може бути справді, але однаково може бути припущенням, заснованим на прагненні до послідовної обробки відповідей, як випливає з назви.

Якщо одночасні дзвінки справді не потрібні, то дивіться відповідь Бенджаміна Грюнбаума, який охоплює послідовні дзвінки (тощо) всебічно.

Якщо ви зацікавлені (для підвищення продуктивності) у моделях, які дозволяють одночасно приймати дзвінки з подальшим послідовним обробкою відповідей, тоді читайте далі.

Привабливо подумати, що вам доведеться використовувати Promise.all(arr.map(fn)).then(fn)(як я це робив багато разів) або фантазійний цукор "Обіцяючої губки" (особливо Bluebird), однак (з урахуванням цієї статті ) arr.map(fn).reduce(fn)модель зробить цю роботу, з перевагами, які вона:

  • працює з будь-якою ланкою для обіцянок - використовується навіть попередня версія jQuery .then().
  • надає гнучкість для пропускання помилок або зупинки на помилку, залежно від того, що вам потрібно з однорядним модом.

Ось це, написано для 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.
    });
};

Примітка: лише той один фрагмент, Q() є специфічним для Q. Для jQuery потрібно переконатися, що readFile () повертає обіцянку jQuery. За допомогою A + lib, іноземні обіцянки будуть засвоєні.

Ключовий момент тут є скороченням в sequenceобіцянці, що послідовності обробки зreadFile обіцянок , але не їх створення.

І як тільки ти це поглинеш, це може бути злегка вражаючим, коли ти зрозумієш, що .map()сцена насправді не потрібна! Всю роботу, паралельні дзвінки плюс послідовне керування в правильному порядку, можна досягти reduce()окремо, плюс додаткова перевага подальшої гнучкості до:

  • конвертувати з паралельних викликів асинхронізації в послідовні виклики асинхронізації шляхом простого переміщення однієї лінії - потенційно корисно під час розробки.

Ось це, Qзнову ж таки.

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

Це основна закономірність. Якщо ви також хотіли доставити дані (наприклад, файли або перетворення їх) абоненту, вам знадобиться м'який варіант.


Я не думаю, що відповідати на питання, що суперечать намірам ОП, не годиться…
Bergi

1
Ця sequence.then(() => filePromise)річ є антипаттерн - вона не поширює помилки, як тільки вони могли (і створює unhandledRejectionв libs, які їх підтримують). Ви скоріше повинні використовувати Q.all([sequence, filePromise])або $.when(sequence, filePromise). Справді, така поведінка може бути тим, чого ви хочете, коли ви прагнете ігнорувати або пропускати помилки, але вам слід принаймні згадати це як недолік.
Бергі

@ Бергі, я сподіваюся, що ОП вступить і винесе рішення про те, чи справді це суперечить його намірам чи ні. Якщо ні, то я видалю відповідь, напевно, тим часом сподіваюся, що я виправдав свою позицію. Дякуємо, що поставились до цього досить серйозно, щоб забезпечити гідні відгуки. Чи можете ви пояснити детальніше про антидіапазон чи надати, будь ласка, посилання? Чи те ж саме стосується статті, де я знайшов основний зразок ?
Roamer-1888

1
Так, третя версія його коду (тобто "паралельна і послідовна") має ту саму проблему. "Антипаттерн" потребує складного поводження з помилками і схильний приєднувати обробники асинхронно, що спричиняє unhandledRejectionподії. У Bluebird можна обійти це, використовуючи те, sequence.return(filePromise)що має таку саму поведінку, але відмінно обробляє відхилення. Я не знаю жодної посилання, я просто придумав це - я не думаю, що "(анти) шаблон" ще не має назви.
Бергі

1
@Bergi, ти чітко бачиш те, чого я не можу :( Цікаво, чи потрібно цей новий антидіапазон десь задокументувати?
Roamer-1888,

0

Ваш підхід непоганий, але у нього є два питання: він заглиблює помилки і використовує явну антивиконну перспективу.

Ви можете вирішити обидва ці проблеми та зробити код чистішим, використовуючи однакову загальну стратегію:

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

Якщо комусь іншому потрібен гарантований спосіб СТРУКТУВАННЯ послідовного способу вирішення обіцянок під час виконання операцій CRUD, ви також можете використовувати наступний код як основу.

Поки ви додасте "return" перед тим, як викликати кожну функцію, описуючи Обіцянку, і використовуєте цей приклад як основу, наступний виклик функції .then () ПОСЛІДНО розпочнеться після завершення попередньої:

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

Для послідовності обіцянок може використовуватися метод масиву push and pop. Ви також можете надсилати нові обіцянки, коли вам потрібні додаткові дані. Це код, який я буду використовувати в завантажувачі React Infinite для завантаження послідовності сторінок.

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

Більшість відповідей не містять результатів ВСІХ обіцянок індивідуально, тому у випадку, якщо хтось шукає саме цю поведінку, це можливе рішення за допомогою рекурсії.

Це відповідає стилю Promise.all:

  • Повертає масив результатів у .then()зворотному дзвінку.

  • Якщо якась обіцянка не виконана, її повернення негайно повертається у .catch()зворотний дзвінок.

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

Зауважте про tasksоголошення масиву :

У цьому випадку неможливо використовувати такі позначення, як, наприклад Promise.all,

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

І ми повинні використовувати:

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

Причина полягає в тому, що JavaScript починає виконувати обіцянку негайно після її оголошення. Якщо ми використовуємо такі методи , як Promise.all, він просто перевіряє , що стан всіх з них fulfilledабо rejected, але не починає сам exection. Використовуючи, () => promise()ми зупиняємо виконання до його виклику.


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

Тут ключовим є те, як ви називаєте функцію сну. Вам потрібно передати масив функцій, який сам повертає обіцянку замість масиву обіцянок.


-1

Це стосується того, як обробляти послідовність обіцянок більш загальним способом, підтримуючи динамічні / нескінченні послідовності, засновані на реалізації spex.sequence :

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

Це рішення не тільки працюватиме з послідовностями будь-якого розміру, але ви можете легко додавати до нього дані і завантажувати балансування .

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.