Master de II. ULL. 1er cuatrimestre
Imagine we are given a piece of code like the one below that uses async functions, how can we rewrite it using only promises and generator functions?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
function doTask1() {
return new Promise((resolve, reject) => {
setTimeout(() => resolve(1), 100)
})
}
function doTask2(arg) {
return new Promise((resolve, reject) => {
setTimeout(() => resolve(arg+2), 100)
})
}
function doTask3(arg) {
return new Promise((resolve, reject) => {
setTimeout(() => resolve(arg+3), 100)
})
}
async function init() {
const res1 = await doTask1();
console.log(res1);
const res2 = await doTask2(res1);
console.log(res2);
const res3 = await doTask3(res2);
console.log(res3);
return res3;
}
init(); // 1\n3\n6
It performs three asynchronous tasks, one after the other where each task depends on the completion of the previous task. Finally, it returns the result of the last task.
How can we rewrite it using generators?
Remember:
next
method.next
method on its iterator-object.next
method is called, its body is executed until the next yield
expression.next()
is always an object with two properties:
value
: the yielded value.done
: true
if the function code has finished, otherwise false
next
method also accepts an argument
.argument
argument
the value of the current yield expression andyield
expressionBy now you would be wondering, how do the generator functions help to achieve our goal?
We need to model an asynchronous flow where we have to wait for certain tasks to finish before proceeding ahead. How can we do that?
Well, the most important insight here is that the generator-functions can yield promises too.
This pattern of weaving a an iterator with yielded promises allows us to model our requirement like this:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
function doTask1(arg) {
return new Promise((resolve, reject) => {
setTimeout(() => resolve(arg), 100)
})
}
function doTask2(arg) {
return new Promise((resolve, reject) => {
setTimeout(() => resolve(arg+2), 100)
})
}
function doTask3(arg) {
return new Promise((resolve, reject) => {
setTimeout(() => resolve(arg+3), 100)
})
}
function* init(arg) {
const res1 = yield doTask1(arg);
console.log(res1);
const res2 = yield doTask2(res1);
console.log(res2);
const res3 = yield doTask3(res2);
console.log(res3);
return res3;
}
Notice how this generator function resembles our async function!
If you change yield
for await
is the same code!
But this is only half the story. Now we need a way to execute its body.
We need a function waiter
that can control the iterator of this generator function to “wait for the fulfillment of the promise yielded on each iteration”. It has to:
Write a function waiter(generator, arg)
that creates and iterator
by calling generator(arg)
and returns a function that traverses the iterator
but proceeding with an iteration only when the promise returned by the previous call to iterator.next()
has been fulfilled. It will be used like this:
1
2
3
4
5
6
function waiter(genFun, arg) {
// ... your code here
}
const doIt = waiter(init, 3);
doIt();
So that, when we run it with the generator above, we obtain:
1
2
3
4
➜ learning-async-iteration-and-generators git:(main) ✗ node 07-async-await-equal-generators-plus-promises/example.js
3
5
8
It sounds complicated, but takes only a few lines to implement.
Heres is a solution