Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Javascript Promise Sequence

I want to process a number of promises in Sequence. I have a working piece of code below but I'm wondering if I have over complicated the chaining of promises. I seem to be creating a great deal of new closures and I'm scratching my head wondering if I'm missing something.

Is there a better way to write this function:

'use strict';
addElement("first")
.then(x => {return addElement("second")})
.then(x => { return addElement("third")})
.then(x => { return addElement("fourth")})   

function addElement(elementText){
    var myPromise = new Promise(function(resolve,reject){
        setTimeout(function(){
            var element=document.createElement('H1');
            element.innerText = `${elementText} ${Date.now()}`;
            document.body.appendChild(element);
            resolve();
        }, Math.random() * 2000);
    });
return myPromise;
}
like image 471
Martin Beeby Avatar asked Mar 03 '16 16:03

Martin Beeby


Video Answer


4 Answers

@TheToolBox has a nice answer for you.

Just for fun, I'm going to show you an alternative technique that uses generators that gets its inspiration from coroutines.

Promise.prototype.bind = Promise.prototype.then;

const coro = g => {
  const next = x => {
    let {done, value} = g.next(x);
    return done ? value : value.bind(next);
  }
  return next();
}

Using that, your code will look like this

const addElement = elementText =>
  new Promise(resolve => {
    setTimeout(() => {
      var element = document.createElement('H1');
      element.innerText = `${elementText} ${Date.now()}`;
      document.body.appendChild(element);
      resolve();
    }, Math.random() * 2000);
  });

coro(function* () {
  yield addElement('first');
  yield addElement('second');
  yield addElement('third');
  yield addElement('fourth');
}());

There's some pretty interesting things you can do using generators with promises. They're not immediately evident here because your addElement promise doesn't resolve any actual values.


If you actually resolve some values, you could do something like

// sync
const appendChild = (x,y) => x.appendChild(y);

// sync
const createH1 = text => {
  var elem = document.createElement('h1');
  elem.innerText = `${text} ${Date.now()}`;
  return elem;
};

// async
const delay = f =>
  new Promise(resolve => {
    setTimeout(() => resolve(f()), Math.random() * 2000);
  });

// create generator; this time it has a name and accepts an argument
// mix and match sync/async as needed
function* renderHeadings(target) {
  appendChild(target, yield delay(() => createH1('first')));
  appendChild(target, yield delay(() => createH1('second')));
  appendChild(target, yield delay(() => createH1('third')));
  appendChild(target, yield delay(() => createH1('fourth')));
}

// run the generator; set target to document.body
coro(renderHeadings(document.body));

Worth noting, createH1 and appendChild are synchronous functions. This approach effectively allows you to chain normal functions together and blur the lines between what is sync and what is async. It also executes/behaves exactly like the code you originally posted.

So yeah, this last code example might be slightly more interesting.


Lastly,

One distinct advantage the coroutine has over the .then chaining, is that all of the resolved promises can be accessed inside the same scope.

Compare .then chains ...

op1()
  .then(x => op2(x))
  .then(y => op3(y))    // cannot read x here
  .then(z => lastOp(z)) // cannot read x or y here

to the coroutine ...

function* () {
  let x = yield op1(); // can read x
  let y = yield op2(); // can read x and y here
  let z = yield op3(); // can read x, y, and z here
  lastOp([x,y,z]);     // use all 3 values !
}

Of course there are workarounds for this using promises, but oh boy does it get ugly fast...


If you are interested in using generators in this way, I highly suggest you checkout the co project.

And here's an article, Callbacks vs Coroutines, from the creator of co, @tj.

Anyway, I hope you had fun learning about some other techniques ^__^

like image 126
Mulan Avatar answered Sep 30 '22 02:09

Mulan


I am not sure why others left out a simple way out, you could simply use an array and reduce method

let promise, inputArray = ['first', 'second', 'third', 'fourth'];

promise = inputArray.reduce((p, element) => p.then(() => addElement(element)), Promise.resolve());
like image 28
mido Avatar answered Sep 30 '22 01:09

mido


Your code looks close to the best you can get here. Promises can be a strange structure to get used to, especially as writing promis-ified code can often end up embedding a function in another function. As you can see here, this is a pretty common phrasing to use. There are only two stylistic changes that could possibly be made. Firstly, myPromise is unnecessary and only serves to add a confusing extra line of code. It's simpler just to return the promise directly. Secondly, you can use function binding to simplify your calls at the beginning. It may not be inside the function itself, but it does eliminate several closures. Both changes are shown below:

'use strict';
addElement("first")
.then(addElement.bind(null,"second"))
.then(addElement.bind(null,"third"))
.then(addElement.bind(null,"fourth"))   

function addElement(elementText){
    return new Promise(function(resolve,reject){
        setTimeout(function(){
            var element=document.createElement('H1');
            element.innerText = `${elementText} ${Date.now()}`;
            document.body.appendChild(element);
            resolve();
        }, Math.random() * 2000);
    });
}

It's worth pointing out that, if you were willing to restructure a bit, a slightly more attractive design would take form:

'use strict';
var myWait = waitRand.bind(null,2000);
myWait
  .then(addElement.bind(null, "first"))
  .then(myWait)
  .then(addElement.bind(null, "second"))
  .then(myWait)
  .then(addElement.bind(null, "third"))

function waitRand(millis) {
  return new Promise((resolve, reject) => {
    setTimeout(resolve, Math.random() * millis);
  }
}

function addElement(elementText) {
  var element = document.createElement('h1');
  element.innerText = `${elementText} ${Date.now()}`;
  document.body.appendChild(element);
}

This trades length of promise chain for clarity, as well as having slightly fewer nested levels.

like image 23
TheToolBox Avatar answered Sep 30 '22 01:09

TheToolBox


You could simplify the use of your function by making addElement() return a function instead so it can be directly inserted into .then() handlers without having to create the anonymous function:

'use strict';
addElement("first")()
  .then(addElement("second"))
  .then(addElement("third"))
  .then(addElement("fourth"))   

function addElement(elementText){
    return function() {
        return new Promise(function(resolve){
            setTimeout(function(){
                var element=document.createElement('H1');
                element.innerText = `${elementText} ${Date.now()}`;
                document.body.appendChild(element);
                resolve();
            }, Math.random() * 2000);
        });
    }
}
like image 28
jfriend00 Avatar answered Sep 30 '22 00:09

jfriend00