@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 ^__^