Разрешать обещания одно за другим (т.е. по порядку)?


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
К вашему сведению, это технически не рекурсия, так как нет наращивания фрейма стека. Предыдущий readFileSequential()уже вернулся до вызова следующего (поскольку он асинхронный, он завершается задолго после того, как исходный вызов функции уже был возвращен).
jfriend00

1
@ jfriend00 Скопление кадров стека не требуется для рекурсии - только самостоятельная ссылка. Это просто техничность, хотя.
Бенджамин Грюнбаум

3
@BenjaminGruenbaum - моя точка зрения заключается в том, что нет ничего плохого в том, что сам вызов функции запускает следующую итерацию. В этом нет никаких недостатков, и, по сути, это эффективный способ последовательности асинхронных операций. Таким образом, нет причин избегать чего-то, что выглядит как рекурсия. Есть рекурсивные решения некоторых проблем, которые неэффективны - это не одна из них.
jfriend00

1
Эй, согласно обсуждению и запросу в комнате JavaScript, я отредактировал этот ответ, чтобы мы могли указать другим на него как на канонический. Если вы не согласны, пожалуйста, дайте мне знать, и я восстановлю его и открою отдельный.
Бенджамин Грюнбаум

Ответы:


338

Обновление 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);
  }
};

Обновление: Вторая мысль - я мог бы вместо этого использовать цикл for:

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ı, нет. Функция "=>" функции стрелки уже подразумевает возврат.
Макс

Если вы используете TypeScript, я думаю, что решение для цикла for является лучшим. Сократить возврат рекурсивных обещаний, например. первый тип возврата вызова - Promise <void>, затем - Promise <Promise <void >> и т. д. - невозможно набирать
текст

@ArturTagisow TypeScript (по крайней мере, новые версии) имеют рекурсивные типы и должны корректно разрешать типы здесь. Обещания <Promise <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)это часть, которую я пропустил, пришлось вручную подталкивать к результатам, что работало, но было менее круто
zavr

Поскольку мы все о современном JS, я считаю, что console.log.bind(console)утверждение в вашем последнем примере теперь обычно не нужно. В эти дни вы можете просто пройти console.log. Например. serial(funcs).then(console.log), Протестировано на текущих nodejs и 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, в ответ на комментарий Данекандо, я думаю, что сокращение должно быть более правильным, выраженным в следующем выражении, вы согласны? 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если файлы являются массивом.
Густаво Родригес

2
Ну ... это ES5. ES6 способ будет for (file of files) {...}.
Густаво Родригес

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

Я представил вариант этого ответа с объяснением ниже. Спасибо
Sarsaparilla

Это именно то, что я делаю в средах до Node 7, не имеющих доступа к async / await. Красиво и чисто.
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

Спасибо за ваш комментарий! Я тоже не использую это имя, но я хотел сделать его более очевидным / простым здесь.
Salketer

5

Самым хорошим решением, которое я смог выяснить, было bluebirdобещание. Вы можете просто сделать то, Promise.resolve(files).each(fs.readFileAsync);что гарантирует, что обещания решаются последовательно по порядку.


1
Еще лучше: Promise.each(filtes, fs.readFileAsync). Кстати, вам не нужно делать .bind(fs)?
Берги

Никто здесь, кажется, не понимает разницу между массивом и последовательностью, что последний подразумевает неограниченный / динамический размер.
виталий-т

Обратите внимание, что массивы в Javascript не имеют ничего общего с массивами фиксированного размера в языках стиля C. Это просто объекты с цифровым управлением ключами, которые не имеют заданного размера или предела ( особенно не при использовании new Array(int). Все, что нужно, - это предварительно установить lengthпару ключ-значение, влияющую на то, сколько индексов используется во время итерации на основе длины. Она имеет ноль). влияние на фактическое индексирование массива или границы индекса)
Майк 'Pomax' Kamermans

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

Это не принципиально отличается от других, опубликованных здесь, но:

  • Применяет функцию к элементам в серии
  • Разрешает массив результатов
  • Не требует async / await (поддержка все еще довольно ограничена, около 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);
};

красиво и легко ... вы должны иметь возможность повторно использовать одно и то же семя для производительности и т. д.

При использовании метода Reduce важно защититься от пустых массивов или массивов, состоящих только из одного элемента , поэтому лучше всего использовать эту технику:

   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 заключается в том, что оно соответствует стилю обещаний. Promise.all и Promise.sequence вызываются одинаково, но имеют разную семантику.

предосторожность

Последовательное выполнение обещаний обычно не очень хороший способ использовать обещания. Обычно лучше использовать Promise.all и позволить браузеру запускать код как можно быстрее. Тем не менее, есть реальные варианты использования для этого - например, при написании мобильного приложения с использованием JavaScript.


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

Кстати, я бы порекомендовал избегать обещания конструктора antipattern
Берги

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

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

Да, если вы считаете, что это библиотечная функция, то все в порядке, но все же в этом случае reduceответ на вопрос Бенджамина просто намного проще.
Берги

2

Вы можете использовать эту функцию, которая получает список PromiseFactories:

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

Promise Factory - это простая функция, которая возвращает Promise:

function myPromiseFactory() {
    return somethingThatCreatesAPromise();
}

Это работает, потому что фабрика обещаний не создает обещание, пока его не попросят. Он работает так же, как и функция then - фактически это то же самое!

Вы вообще не хотите оперировать множеством обещаний. Согласно спецификации 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

Используя современные ES:

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)

Без Async / Await (используя Promise)

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(в соответствии с этим )
Гил Эпштейн

0

Исходя из заголовка вопроса «Разрешать обещания одно за другим (т.е. по порядку)?», Мы можем понять, что ФП больше заинтересован в последовательной обработке обещаний по расчету, чем в последовательных вызовах как таковых .

Этот ответ предлагается:

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

Если одновременные вызовы действительно нежелательны, см. Ответ Бенджамина Грюнбаума, который полностью охватывает последовательные вызовы (и т. Д.).

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

Соблазнительно думать, что вы должны использовать Promise.all(arr.map(fn)).then(fn)(как я это делал много раз) или необычный сахар в Promise lib (особенно Bluebird), однако (с учетом этой статьи ) arr.map(fn).reduce(fn)шаблон будет работать с преимуществами, которые он дает :

  • работает с любым обещанием lib - даже с предварительно совместимыми версиями 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 + libs, иностранные обещания будут ассимилированы.

Ключевой момент здесь является сокращением в 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.
    });
};

Это основной шаблон. Если вы хотите также передать данные (например, файлы или их преобразование) вызывающей стороне, вам понадобится мягкий вариант.


Я не думаю, что это хорошая идея, чтобы отвечать на вопросы вопреки намерениям ОП ...
Берги

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

@ Берджи, я надеюсь, что ОП вмешается и вынесет решение о том, действительно ли это противоречит его намерениям или нет. Если нет, я удалю ответ, который, я думаю, тем временем надеюсь, что оправдал свою позицию. Спасибо, что приняли это достаточно серьезно, чтобы обеспечить достойную обратную связь. Не могли бы вы подробнее рассказать об анти-паттерне или дать ссылку? Относится ли то же самое к статье, где я нашел основной шаблон ?
Roamer-1888

1
Да, третья версия его кода («параллельная и последовательная») имеет ту же проблему. «Антипаттерн» требует сложной обработки ошибок и склонен асинхронно подключать обработчики, что вызывает unhandledRejectionсобытия. В Bluebird вы можете обойти это, используя sequence.return(filePromise)то же поведение, но отлично обрабатывая отклонения. Я не знаю ни одной ссылки, я только придумал это - я не думаю, что у «(анти) образца» еще есть имя.
Берги

1
@ Берги, вы можете ясно видеть то, что я не могу :( Интересно, нужно ли где-то документировать этот новый анти-паттерн?
Roamer-1888

0

Ваш подход не плохой, но у него есть две проблемы: он глотает ошибки и использует антипаттерн Explicit Promise Construction.

Вы можете решить обе эти проблемы и сделать код чище, используя при этом одну и ту же общую стратегию:

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

Если кому-то еще требуется гарантированный СТРОГО последовательный способ разрешения Promises при выполнении операций CRUD, вы также можете использовать следующий код в качестве основы.

До тех пор, пока вы добавите 'return' перед вызовом каждой функции, описывающей Promise, и будете использовать этот пример в качестве основы, следующий вызов функции .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 и 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, но не запускает само выполнение. С помощью () => 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.