Javascript Map that waits for previous promise bef

2019-05-10 08:05发布

问题:

I know this isn't in the scope of a Array.map but I'd like to wait until the previous item has finished its promise before starting the next one. It just happens that I need to wait for the previous entry to be saved in the db before moving forwards.

const statsPromise = stats.map((item) => {
    return playersApi.getOrAddPlayer(item, clubInfo, year); //I need these to wait until previous has finished its promise.
});

Promise.all(statsPromise)
.then((teamData) => {
  ..//
});

playersApi.getOrAddPlayer returns a new Promise

Edit

Reading more on it, it seems its important to show playersApi.getOrAddPlayer

getOrAddPlayer: function (item, clubInfo, year) {
    return new Promise((resolve, reject) => {

        var playerName = item.name.split(' '),
            fname = playerName[0].caps(),
            sname = playerName[1].caps();

                Players.find({
                    fname: fname,
                    sname: sname,
                }).exec()
                .then(function(playerDetails, err){
                    if(err) reject(err);
                    var savePlayer = new Players();
                    //stuff
                    savePlayer.save()
                    .then(function(data, err){
                        if(err)  reject(err);
                        item._id = data._id;
                        resolve(item);
                    });
                });
            });
}

回答1:

You can use reduction instead of mapping to achieve this:

stats.reduce(
  (chain, item) =>
    // append the promise creating function to the chain
    chain.then(() => playersApi.getOrAddPlayer(item, clubInfo, year)),
  // start the promise chain from a resolved promise
  Promise.resolve()
).then(() => 
  // all finished, one after the other
);

Demonstration:

const timeoutPromise = x => {
  console.log(`starting ${x}`);
  return new Promise(resolve => setTimeout(() => {
    console.log(`resolving ${x}`);
    resolve(x);
  }, Math.random() * 2000));
};

[1, 2, 3].reduce(
  (chain, item) => chain.then(() => timeoutPromise(item)),
  Promise.resolve()
).then(() =>
  console.log('all finished, one after the other')
);

If you need to accumulate the values, you can propagate the result through the reduction:

stats
  .reduce(
    (chain, item) =>
      // append the promise creating function to the chain
      chain.then(results =>
        playersApi.getOrAddPlayer(item, clubInfo, year).then(data =>
          // concat each result from the api call into an array
          results.concat(data)
        )
      ),
    // start the promise chain from a resolved promise and results array
    Promise.resolve([])
  )
  .then(results => {
    // all finished, one after the other
    // results array contains the resolved value from each promise
  });

Demonstration:

const timeoutPromise = x => {
  console.log(`starting ${x}`);
  return new Promise(resolve =>
    setTimeout(() => {
      console.log(`resolving result for ${x}`);
      resolve(`result for ${x}`);
    }, Math.random() * 2000)
  );
};

function getStuffInOrder(initialStuff) {
  return initialStuff
    .reduce(
      (chain, item) =>
        chain.then(results =>
          timeoutPromise(item).then(data => results.concat(data))
        ),
      Promise.resolve([])
    )
}

getStuffInOrder([1, 2, 3]).then(console.log);


Variation #1: Array.prototype.concat looks more elegant but will create a new array on each concatenation. For efficiency purpose, you can use Array.prototype.push with a bit more boilerplate:

stats
  .reduce(
    (chain, item) =>
      chain.then(results =>
        playersApi.getOrAddPlayer(item, clubInfo, year).then(data => {
          // push each result from the api call into an array and return the array
          results.push(data);
          return results;
        })
      ),
    Promise.resolve([])
  )
  .then(results => {

  });

Demonstration:

const timeoutPromise = x => {
  console.log(`starting ${x}`);
  return new Promise(resolve =>
    setTimeout(() => {
      console.log(`resolving result for ${x}`);
      resolve(`result for ${x}`);
    }, Math.random() * 2000)
  );
};

function getStuffInOrder(initialStuff) {
  return initialStuff
    .reduce(
      (chain, item) =>
        chain.then(results =>
          timeoutPromise(item).then(data => {
            results.push(data);
            return results;
          })
        ),
      Promise.resolve([])
    );
}

getStuffInOrder([1, 2, 3]).then(console.log);


Variation #2: You can lift the results variable to the upper scope. This would remove the need to nest the functions to make results available via the nearest closure when accumulating data and instead make it globally available to the whole chain.

const results = [];
stats
  .reduce(
    (chain, item) =>
      chain
        .then(() => playersApi.getOrAddPlayer(item, clubInfo, year))
        .then(data => {
          // push each result from the api call into the globally available results array
          results.push(data);
        }),
    Promise.resolve()
  )
  .then(() => {
    // use results here
  });

Demonstration:

const timeoutPromise = x => {
  console.log(`starting ${x}`);
  return new Promise(resolve =>
    setTimeout(() => {
      console.log(`resolving result for ${x}`);
      resolve(`result for ${x}`);
    }, Math.random() * 2000)
  );
};

function getStuffInOrder(initialStuff) {
  const results = [];
  return initialStuff.reduce(
    (chain, item) =>
      chain
        .then(() => timeoutPromise(item))
        .then(data => {
          results.push(data);
          return results;
        }),
    Promise.resolve()
  );
}

getStuffInOrder([1, 2, 3]).then(console.log);




回答2:

If you are fine with using promise library, you can use Promise.mapSeries by Bluebird for this case.

Example:

const Promise = require("bluebird");
//iterate over the array serially, in-order
Promise.mapSeries(stats, (item) => {
  return playersApi.getOrAddPlayer(item, clubInfo, year));
}).then((teamData) => {
  ..//
});


回答3:

You can use a recursion solution

const statsPromise = (function s(p, results) {
  return p.length ? playersApi.getOrAddPlayer(p.shift(), clubInfo, year) : results;
})(stats.slice(0), []);

statsPromise
.then((teamData) => {
//do stuff
});

let n = 0;
let promise = () => new Promise(resolve => 
                setTimeout(resolve.bind(null, n++), 1000 * 1 + Math.random()));

let stats = [promise, promise, promise];

const statsPromise = (function s(p, results) {
  return p.length ? p.shift().call().then(result => {
    console.log(result);
    return s(p, [...results, result])
  }) : results;
})(stats.slice(0), []);
    
statsPromise.then(res => console.log(res))



回答4:

You could use a kind of recursion:

function doStats([head, ...tail]) {
  return !head ? Promise.resolve() :
    playersApi.getOrAddPlayer(head, clubInfo, year)
      .then(() => doStats(tail));
}

doStats(stats)
  .then(() => console.log("all done"), e => console.log("something failed", e));

Another classic approach is to use reduce:

function doStats(items) {
  return items.reduce(
    (promise, item) => 
      promise.then(() => playersApi.getOrAddPlayer(item, clubInfo, year)),
    Promise.resolve());

By the way, you could clean up your getOrAddPlayer function quite a bit, and avoid the promise constructor anti-pattern, with:

getOrAddPlayer: function (item, clubInfo, year) {
    var playerName = item.name.split(' '),
        fname = playerName[0].caps(),
        sname = playerName[1].caps();

    return Players.find({fname, sname}).exec()
      .then(playerDetails => new Players().save())
      .then({_id} => Object.assign(item, {_id}));
}


回答5:

I gave it a thought but I didn't find a better method than the reduce one.

Adapted to your case it would be something like this:

const players = [];
const lastPromise = stats.reduce((promise, item) => {
  return promise.then(playerInfo => {
    // first iteration will be undefined
    if (playerInfo) {
       players.push(playerInfo)
    }
    return playersApi.getOrAddPlayer(item,  clubInfo, year);
  });
}, Promise.resolve());

// assigned last promise to a variable in order to make it easier to understand
lastPromise.then(lastPlayer => players.push(lastPlayer));

You can see some explanation about this here.