Previous month I've been attending Kyle Simpson workshop "JavaScript: The Recent Parts" in Amsterdam in June, which I enjoyed so much. The material and the show was great, and Kyle is a real master in performing workshops!
One of the most interesting topics I was excited about in the workshop was about async generators
just at the end of the event. The audience was intrigued and had some questions regarding different strategies and iterations order that can be achieved with async generators
technique.
In order to understand it better myself, I've decided to write a short medium post about these patterns. In this post I will explain some concepts related to generators
topic and describe these strategies in details.
There are plenty of nice articles and tutorials explaining Iterators in details. So, I will do it very briefly for the sake of staying closer to the article's goal.
Iterators are in a nutshell any objects implementing the Iterator protocol. Basically, an Iterator
object must contain a next
function, returning a value
of any type and a Boolean done
properties. The next
method defines an order on which iteration is happening along with values, produced on every step.
const iterable = {
[Symbol.iterator]() {
let step = 0
let done = false
let value
return {
next() {
step++
switch (step) {
case 1:
value = 'hello'
break
case 2:
value = 'world'
break
default:
done = true
}
return { done, value }
}
}
}
}
To iterate over such an object it's possible to use different techniques:
- A one is the
for-of
statement, introduced in ECMAScript 6 update
for (const a of iterable) {
console.log(a)
}
// -->
// "hello"
// "world"
- The spread operator helps to convert iterable object into an array.
console.log([...iterable])
// -->
// ["hello", "world"]
// you can try it here - https://jsbin.com/suxikofesa/edit?js,console
Many JavaScript
built-in objects, like arrays, strings, maps, and sets implement iterable interface already. Usually, and to be compatible with generators
, iterators are declared on a Symbol.iterator
property.
const str = 'hi'
const iterator = str[Symbol.iterator]()
iterator.next() // {value: "h", done: false}
iterator.next() // {value: "i", done: false}
iterator.next() // {value: undefined, done: true}
iterator.next() // {value: undefined, done: true}
You might notice, that after reaching the end state, after done appears to be true
a next()
call to iterator method does return the same result - {value: undefined, done: true}
.
A generator is a function, which simplifies iterators declaration. It can stop it's execution and return multiple values.
function* iterator() {
yield 1
yield 2
yield 3
}
for (const value of iterator()) {
console.log(value)
}
// -->
// 1
// 2
// 3
A generator starts with function*
operator, and it's body is almost like in a normal function, extended with a few features.
yield
statement should be presented and it stops a function invocation, returning a specified value. The*yield
flavour delegates or flats nested generators.
Async Iterators allow to delay an iteration process involving asycronous logic.
Each result of an async iterator next()
method returns a Promise
, resolved into an object, containing value
and done
properties, as usual.
Async functions allow to use await
syntax and return Promise
. Logically, async generators permit await
and yield Promises
. 🦄 💬 💬
So, why would someone use async generators?
Good question! Async iterators
come with a cool loop construction
for await (const item of asyncIterator) {
// ...
}
That's why! ;) Meaning, a developer can define any asyncronous iteration order he or she likes to. For instance, iterate over Node read streams.
Don't forget to switch to a newer version of Node
:
nvm use 12
Now using node v12.4.0 (npm v6.9.0)
const fs = require('fs')
const readStream = fs.createReadStream('./test.txt', { encoding: 'utf8' })
async function print() {
for await (const chunk of readStream) {
console.log(chunk)
}
}
print()
Please note, that for-await
can be used only inside async
function.
And, implementation-wise, async iterators
should be declared with Symbol.asyncIterator
properties.
Imagine, you want to request multiple asyncronous sources:
async function* inOrderRequests(urls) {
for (const url of urls) {
const response = await fetch(url)
const text = await response.text()
yield text
}
}
Now, it is possible to iterate urls
over in a async function:
async function pageText(urls) {
for await (const responseText of inOrderRequests(urls)) {
console.log(responseText)
}
}
pageText([location.href])
pageText([location.href])
outputs the initial text of a current browser page.
During the workshop, Kyle identified three different strategies, can be used for requesting asyncronous resources:
in order - in order
when a request is performed after the other request's response is returned. ThepageText
above is an example of such a strategy.asap - in order
when requests are performed all together, and responses are returned in the initial order. We'll modify a previous example with anasapInOrderRequests
function.
async function* asapInOrderRequests(urls) {
const requests = urls.map((url) => fetch(url))
for await (const response of requests) {
const text = await response.text()
yield text
}
}
async function pageText(urls) {
for await (const responseText of asapInOrderRequests(urls)) {
console.log(responseText)
}
}
Now, all the requests are performed at once. The function waits for each request to resolve, but overall time spent by requests limited by it's the most delayed Promise
.
asap - asap
strategy is applied when both requests and results are performed and returned as soon as possible.
On the workshop Kyle showed all three possibilities, where asap - asap
was the most difficult, as he was improvising. I do find the latter the most interesting too
https://gist.github.com/korzio/52774b2cf2281662ca0e6ef4600bf6bd
In this example the JavaScript closure feature is used to signal of a lately resolved Promise
. Whenever it is fulfilled, generator yields
the result, so the full race of Promises
is happening. It took me some time and attempts to finish the exercise. Hopefully, you find it usefull.
I'm very interested in your feedback. Please let me know what do you think of that piece of code. Whould do you want to use a similar construction in your project? See you in comments :)
Thank you for your time and have a nice coding!