Understanding generator

Where it comes from

From MDN:

The Generator object is returned by a generator function and it conforms to both the iterable protocol and the iterator protocol.

This object cannot be instantiated directly. Instead, a Generator instance can be returned from a generator function:

1
2
3
4
5
6
7
function* generator() {
yield 1
yield 2
yield 3
}

const gen = generator() // "Generator { }"

How it runs

It pauses at yield keyword and can resume from there.

If there is:

1
2
y = yield 1
yield y

When calling gen.next(2), generator will yield 1 and pause before the y = assigning. And then when calling gen.next(), generator will assign y = 2 and yield 2.

async await are based on generator

Definition

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
33
34
35
36
37
function _asyncToGenerator(fn) {
// fn is a generator function
return () => {
var self = this,
args = arguments

return new Promise(function (resolve, reject) {
// gen is a generator
var gen = fn.apply(self, args)
function _next(value) {
asyncGeneratorStep(gen, resolve, reject, _next, _throw, 'next', value)
}
function _throw(err) {
asyncGeneratorStep(gen, resolve, reject, _next, _throw, 'throw', err)
}
_next(undefined)
})
}
}

function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) {
let info, value
try {
info = gen[key](arg) // invoke next or throw function on the generator object with arg
value = info.value // info = {value: _, done: _}. Note that value is a promise
} catch (error) {
reject(error)
return
}

if (info.done) {
resolve(value) // if generator has finished, resolve the promise returned by _asyncToGenerator
} else {
// if generator has not finished, recursively iterate to the next in gen
Promise.resolve(value).then(_next, _throw)
}
}

Usage

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const asyncFunc = _asyncToGenerator(function* () {
console.log(1)
yield new Promise((resolve) => {
setTimeout(() => {
resolve()
console.log('sleep 1s')
}, 1000)
})
console.log(2)
const a = yield Promise.resolve('a')
console.log(3)
const b = yield Promise.resolve('b')
const c = yield Promise.resolve('c')
return [a, b, c]
})

asyncFunc().then((res) => {
console.log(res)
})

Same output as:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const asyncFunc = async () => {
console.log(1)
await new Promise((resolve) => {
setTimeout(() => {
resolve()
console.log('sleep 1s')
}, 1000)
})
console.log(2)
// each await is a yield statement in the generator function
// with await, the async function resumes to execute to the next line when promise resolves
// with generator, the generator iterates to next when promise resolves
const a = await Promise.resolve('a')
console.log(3)
const b = await Promise.resolve('b')
const c = await Promise.resolve('c')
return [a, b, c]
}

func().then((res) => {
console.log(res)
})

An interesting observation on promise being flattened

I noticed that if instead of returning [a, b, c] in the end, I return another promise that wraps multiple layers of promise, resolves to [a,b,c], the .then will still resolve to [a, b, c].
This is because Promise.resolve() flattens the promise:

This function flattens nested layers of promise-like objects (e.g. a promise that resolves to a promise that resolves to something) into a single layer.

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/resolve