admin管理员组

文章数量:1125947

Consider the following code that reads an array of files in a serial/sequential manner. readFiles returns a promise, which is resolved only once all files have been read in sequence.

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 with the first file!
  });
};

The above code works, but I don't like having to do recursion for things to occur sequentially. Is there a simpler way that this code can be re-written so that I don't have to use my weird readSequential function?

Originally I tried to use Promise.all, but that caused all of the readFile calls to happen concurrently, which is not what I want:

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

Consider the following code that reads an array of files in a serial/sequential manner. readFiles returns a promise, which is resolved only once all files have been read in sequence.

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 with the first file!
  });
};

The above code works, but I don't like having to do recursion for things to occur sequentially. Is there a simpler way that this code can be re-written so that I don't have to use my weird readSequential function?

Originally I tried to use Promise.all, but that caused all of the readFile calls to happen concurrently, which is not what I want:

var readFiles = function(files) {
  return Promise.all(files.map(function(file) {
    return readFile(file);
  }));
};
Share Improve this question edited Feb 9, 2021 at 4:40 Kevin Buchs 2,8525 gold badges42 silver badges61 bronze badges asked Jul 5, 2014 at 11:48 XåpplI'-I0llwlg'I -XåpplI'-I0llwlg'I - 22.4k28 gold badges107 silver badges154 bronze badges 8
  • 3 Anything that has to wait for a previous asynchronous operation to finish has to be done in a callback. Using promises doesn't change that. So you need the recursion. – Barmar Commented Jul 5, 2014 at 11:53
  • 2 FYI, this isn't technically recursion as there is no stack frame build-up. The previous readFileSequential() has already returned before the next one is called (because it's async, it completes long after the original function call has already returned). – jfriend00 Commented Jul 5, 2014 at 15:58
  • 1 @jfriend00 Stack frame accumulation is not required for recursion - only self reference. This is just a technicality though. – Benjamin Gruenbaum Commented Jul 5, 2014 at 18:49
  • 4 @BenjaminGruenbaum - my point is that there is absolutely nothing wrong with having the function call itself to kick off the next iteration. There is zero downside to it and, in fact, it's an efficient way to sequence async operations. So, there's no reason to avoid something that looks like recursion. There are recursive solutions to some problems that are inefficient - this is not one of those. – jfriend00 Commented Jul 5, 2014 at 18:56
  • 1 Hey, per a discussion and request in the JavaScript room I've edited this answer so we can point others to it as a canonical. If you disagree please let me know and I'll restore it and open a separate one. – Benjamin Gruenbaum Commented Aug 27, 2018 at 12:50
 |  Show 3 more comments

36 Answers 36

Reset to default 1 2 Next 506

Update 2017: I would use an async function if the environment supports it:

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

If you'd like, you can defer reading the files until you need them using an async generator (if your environment supports it):

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

Update: In second thought - I might use a for loop instead:

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

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

Or more compactly, with reduce:

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

In other promise libraries (like when and Bluebird) you have utility methods for this.

For example, Bluebird would be:

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

Although there is really no reason not to use async await today.

This question is old, but we live in a world of ES6 and functional JavaScript, so let's see how we can improve.

Because promises execute immediately, we can't just create an array of promises, they would all fire off in parallel.

Instead, we need to create an array of functions that returns a promise. Each function will then be executed sequentially, which then starts the promise inside.

We can solve this a few ways, but my favorite way is to use reduce.

It gets a little tricky using reduce in combination with promises, so I have broken down the one liner into some smaller digestible bites below.

The essence of this function is to use reduce starting with an initial value of Promise.resolve([]), or a promise containing an empty array.

This promise will then be passed into the reduce method as promise. This is the key to chaining each promise together sequentially. The next promise to execute is func and when the then fires, the results are concatenated and that promise is then returned, executing the reduce cycle with the next promise function.

Once all promises have executed, the returned promise will contain an array of all the results of each promise.

ES6 Example (one liner)

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

ES6 Example (broken down)

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

Usage:

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

Here is how I prefer to run tasks in series.

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

What about cases with more tasks? Like, 10?

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

To do this simply in 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;
}

Addition example

const addTwo = async () => 2;

const addThree = async (inValue) => new Promise((resolve) => setTimeout(resolve(inValue + 3), 2000));

const addFour = (inValue) => new Promise((res) => setTimeout(res(inValue + 4), 1000)); 

const addFive = async (inValue) => inValue + 5;

// Function which handles promises from above
async function sequenceAddition() {
  let sum = await [addTwo, addThree, addFour, addFive].reduce(
    (promise, currPromise) => promise.then((val) => currPromise(val)),
    Promise.resolve()
  );
  console.log('sum:', sum); // 2 + 3 + 4 + 5 =  14
}

// Run function. See console for result.
sequenceAddition();

General syntax to use reduce()

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

UPDATE

items-promise is a ready to use NPM package doing the same.

I've had to run a lot of sequential tasks and used these answers to forge a function that would take care of handling any sequential task...

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

The function takes 2 arguments + 1 optional. First argument is the array on which we will be working. The second argument is the task itself, a function that returns a promise, the next task will be started only when this promise resolves. The third argument is a callback to run when all tasks have been done. If no callback is passed, then the function returns the promise it created so we can handle the end.

Here's an example of usage:

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

Hope it saves someone some time...

With Async/Await (if you have the support of ES7)

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

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

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

(you must use for loop, and not forEach because async/await has problems running in forEach loop)

Without Async/Await (using 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);
}

First, you need to understand that a promise is executed at the time of creation.
So for example if you have a code:

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

You need to change it to:

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

Then we need to sequentially chain promises:

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

executing after(), will make sure that promise is created (and executed) only when its time comes.

My preferred solution:

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

It's not fundamentally different from others published here but:

  • Applies the function to items in series
  • Resolves to an array of results
  • Doesn't require async/await (support is still quite limited, circa 2017)
  • Uses arrow functions; nice and concise

Example usage:

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

Tested on reasonable current Chrome (v59) and NodeJS (v8.1.2).

With async/await of ES2016 (and maybe some features of ES2018), this can be reduced to this form:

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

async function readFiles(files) {
  for (file in files) {
     await readFile(file)
  }
}

I haven't seen another answer express that simplicity. The OP said parallel execution of readFile was not desired. However, with IO like this it really makes sense to not be blocking on a single file read, while keeping the loop execution synchronous (you don't want to do the next step until all files have been read). Since I just learned about this and am a bit excited about it, I'll share that approach of parallel asynchronous execution of readFile with overall synchronous execution of readFiles.

async function readFiles(files) {
  await Promise.all(files.map(readFile))
}

Isn't that a thing of beauty?

Nicest solution that I was able to figure out was with bluebird promises. You can just do Promise.resolve(files).each(fs.readFileAsync); which guarantees that promises are resolved sequentially in order.

This is a slight variation of another answer above. Using native Promises:

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

Explanation

If you have these tasks [t1, t2, t3], then the above is equivalent to Promise.resolve().then(t1).then(t2).then(t3). It's the behavior of reduce.

How to use

First You need to construct a list of tasks! A task is a function that accepts no argument. If you need to pass arguments to your function, then use bind or other methods to create a task. For example:

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

Most of the answers dont include the results of ALL promises individually, so in case someone is looking for this particular behaviour, this is a possible solution using recursion.

It follows the style of Promise.all:

  • Returns the array of results in the .then() callback.

  • If some promise fails, its returned immediately in the .catch() callback.

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 

本文标签: javascriptResolve promises one after another (ie in sequence)Stack Overflow