Разрешать promises один за другим (т.е. Последовательно)?

Рассмотрим следующий код, который читает массив файлов в последовательном/последовательном порядке. 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 функцию readSequential?

Первоначально я пытался использовать Promise.all, но это привело к одновременному выполнению readFile вызовов readFile, а это не то, чего я хочу:

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

Ответ 1

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

Вот как я предпочитаю запускать задачи последовательно.

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

Ответ 3

Этот вопрос старый, но мы живем в мире ES6 и функциональном JavaScript, поэтому давайте посмотрим, как мы можем улучшить.

Поскольку promises выполняется немедленно, мы не можем просто создать массив из promises, все они будут сработаны параллельно.

Вместо этого нам нужно создать массив функций, который возвращает обещание. Затем каждая функция будет выполняться последовательно, а затем запускает обещание внутри.

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

Немного сложно использовать reduce в комбинации с promises, поэтому я сломал один вкладыш на несколько менее удобоваримых укусов ниже.

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

Это обещание будет затем передано в метод reduce как promise. Это ключ к последовательному объединению каждого обещания. Следующее обещание выполнить: func и когда срабатывает then, результаты конкатенируются и затем возвращаются обещание, выполняя цикл reduce со следующей функцией обещания.

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

Пример 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))

Ответ 4

Для этого просто в 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(function(file) {

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

    })

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

Ответ 5

Простая утилита для стандартного обещания Node.js:

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

ОБНОВИТЬ

items-обещание - готовый пакет NPM, делающий то же самое.

Ответ 6

Мне пришлось запускать много последовательных задач и использовать эти ответы для создания функции, которая позаботится о обработке любой последовательной задачи...

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

Надеюсь, что это кому-то поможет...

Ответ 7

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

Ответ 8

Это небольшая вариация другого ответа выше. Использование native Promises:

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(...)

Ответ 9

Мое предпочтительное решение:

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

Ответ 10

Используйте 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
});

Ответ 11

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

Внимание!

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

Ответ 12

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

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

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

function myPromiseFactory() {
    return somethingThatCreatesAPromise();
}

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

Вы не хотите работать над массивом promises вообще. По спецификации Promise, как только создается обещание, оно начинает выполняться. Так что вы действительно хотите, это массив обещающих фабрик...

Если вы хотите узнать больше о Promises, вы должны проверить эту ссылку: https://pouchdb.com/2015/05/18/we-have-a-problem-with-promises.html

Ответ 13

Мой ответ основан на fooobar.com/questions/44161/....

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

Ответ 14

Мне очень понравился ответ @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 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 display the final value here
    console.log('=== The final result ===')
    console.log(result)
  })

Ответ 15

Как заметил Берги, я думаю, что лучшее и понятное решение - использовать BlueBird.each, код ниже:

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

Ответ 16

Я использую следующий код для расширения объекта Promise. Он обрабатывает отказ от promises и возвращает массив результатов

код

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

Ответ 17

Если вы хотите, вы можете использовать сокращение, чтобы сделать последовательное обещание, например:

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

он всегда будет работать последовательно.

Ответ 18

Я не понимаю, почему люди предлагают такие сложные решения. Вот более простая логика

С Async/Await - лучшее решение, если у вас есть поддержка ES7

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

async function main()
{
  var filesList = [...];
  for (var i = 0; i <= filesList.length; i++)
  {
    await downloadFile(filesList[i]);
  }
}

Без асинхронного/ожидающего

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

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

function main()
{
  var filesList = [...];
  downloadFile(filesList, 0);
}

Ответ 19

На основе заголовка вопроса: "Разрешите promises один за другим (то есть в последовательности)?", мы могли бы понять, что OP больше интересуется последовательной обработкой promises при расчете, чем последовательные вызовы в как таковые.

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

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

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

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

Заманчиво думать, что вам нужно использовать Promise.all(arr.map(fn)).then(fn) (как я уже много раз делал) или сабвуфер Promise lib (особенно Bluebird's), однако (с учетом в этой статье) шаблон 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, иностранный promises будет ассимилироваться.

Ключевым моментом здесь является сокращение sequence обещание, которое упорядочивает обработку readFile promises, но не их создание.

И как только вы впитаете это, возможно, слегка раздумывая, когда понимаете, что этап .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.
    });
};

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

Ответ 20

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

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

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

Ответ 21

Если кому-то еще требуется гарантированный СТРОГО последовательный способ разрешения 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);
        });    
    });
}

Ответ 22

Для последовательности обещаний можно использовать метод массива 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);

Ответ 23

Это будет расширено, как обрабатывать последовательность promises более общим образом, поддерживая динамические/бесконечные последовательности на основе 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;
    });

Не только это решение будет работать с последовательностями любого размера, но вы можете легко добавить дросселирование и балансировку нагрузки к нему.