Async/await, a more compelling example
jeudi 5 octobre 2017
Most of the introductions to async/await in JavaScript that I’ve read are very similar, something like this:
// Promise version
function insertArticle(url, element) {
return fetch(url)
.then(response => response.json())
.then(data => {
const article = document.createTextNode(data.article)
element.appendChild(article)
})
.catch(reason => console.error(reason));
}
// Async/await version
async function insertArticle(url, element) {
try {
const response = await fetch(url)
const data = await response.json()
const article = document.createTextNode(data.article)
element.appendChild(article)
} catch (reason) {
console.error(reason)
}
}
Sure, the async version looks nicer but it’s not earth shattering. When deciding whether to use it or not, it’s a tiny argument in the pro column. In the cons, you have the reduced compatibility with browsers that do not support it yet or the extra code that a polyfill would bring along.
A more compelling example
A couple of weeks ago, Benjamin De Cock shared a snippet to simulate realistic typing. As I studied it, I felt that the code was not representing the underlying algorithm: for each character in the text, we wait a random time then write the character. Pretty simple to explain, right? Let’s use it as an exercise to get familiar with async/await’s potential.
First version, callbacks
I rewrote Benjamin’s original version a little bit. I’ve simplified the tracking of time, changed the function signature and removed the ability to chain calls. But the gist of the algorithm is the same.
function delay(callback, duration) {
const startTime = performance.now()
const tick = () => {
if (performance.now() - startTime < duration) {
requestAnimationFrame(tick)
} else {
callback()
}
}
tick()
}
function random(min, max) {
return Math.random() * (max - min) + min
}
function simulateTyping(
text,
{
in: target,
callback,
min = 10,
max = 80,
iterator = text[Symbol.iterator](),
}
) {
const { value } = iterator.next()
if (!value) {
return callback && callback()
}
delay(() => {
target.insertAdjacentText('beforeend', value)
simulateTyping(text, { in: target, callback, min, max, iterator })
}, random(min, max))
}
simulateTyping('hello world', {
in: document.querySelector('.fake-input'),
callback: () => {
console.log('done')
},
})
When you read that, it is pretty difficult to follow along. The iteration on each character is done via recursions with an iterator. The delay between each character is also measured with callbacks. There’s a big disconnect between the way a human thinks and the translation of that thinking in code.
Second version, promises
Let’s see if using promises instead of callbacks simplifies the code.
function nextFrame() {
return new Promise(resolve => {
requestAnimationFrame(resolve)
})
}
function randomDelay(min, max) {
const delay = Math.random() * (max - min) + min
const startTime = performance.now()
return nextFrame().then(() => {
if (performance.now() - startTime < delay) {
return nextFrame()
}
})
}
function simulateTyping(
text,
{ in: target, min = 10, max = 80, iterator = text[Symbol.iterator]() }
) {
const { value } = iterator.next()
if (!value) {
return Promise.resolve()
}
return randomDelay(min, max).then(() => {
target.insertAdjacentText('beforeend', value)
simulateTyping(text, { in: target, min, max, iterator })
})
}
simulateTyping('hello world', {
in: document.querySelector('.fake-input'),
}).then(() => {
console.log('done')
})
This has a bit less boilerplate. We don’t have to pass a callback around so we can simplify some calls. But overall, this is the same structure and still difficult.
Third version, async/await
async function nextFrame() {
return new Promise(resolve => {
requestAnimationFrame(resolve)
})
}
async function randomDelay(min, max) {
const delay = Math.random() * (max - min) + min
const startTime = performance.now()
while (performance.now() - startTime < delay) {
await nextFrame()
}
}
async function simulateTyping(text, { in: target, min = 10, max = 80 }) {
for (const character of text) {
await randomDelay(min, max)
target.insertAdjacentText('beforeend', character)
}
}
simulateTyping('hello world', {
in: document.querySelector('.fake-input'),
}).then(() => {
console.log('done')
})
Now that is much clearer! Not very surprising since this is the whole point of this post. The code uses a for/of loop to write each character and a while loop to wait for each random delay.
Doing this little exercise has helped me appreciate async/await more than most things I’ve read so far. Have you seen other nice examples of async/await out there?