When executing long-running JavaScript code the web browser's User Interface (UI) remains unresponsive thanks to the infamous single-threaded nature of JavaScript. Consequently it's useful in time-consuming JavaScript code, to defer back to the UI from time to time, to let user events like scrolling, clicking and typing all take their natural course.
That proves to be quite a tricky thing. Or not, depending on how you look at it.
Mario Figueiredo provides a solution on Stack Overflow and I recast it here, essentially as follows:
function defer_to_UI(how_long = 0) {
return new Promise(resolve => setTimeout(resolve, how_long));
}
You have to love JavaScript no? Such abundant clarity not? (and I've taken liberties to clarify Mario's code a little too).
So what is actually going on here?
setTimeout
, as it happens, schedules something to happen in the future by a specified number of milliseconds (how_long
) and if that is 0ms it schedules it to happen ASAP in the future. What does that mean?
Recalling that JavaScript is essentially single-threaded, what this does is put the call to resolve()
on a queue. The same queue as it happens that UI events are sitting in, waiting to be handled.
In the context of the long running style in-lining code I'm running, they are not being handled as the styles are being in-lined. Because the in-lining function is running, they are waiting patiently in the queue until it's done. Which as you may recall could be 30 seconds.
setTimeout()
is a function that asks the browser to run a timer (for how_long
microseconds), and when the timer is up, to place a call to the function (resolve()
in this case) on the event loop queue. But the function resolve
is a mystery here. It is provided as the argument to a function that is wrapped in a promise. Wowsers, what?
Promises, promises
A promise is just a JavaScript object that maintains a state of either "pending", "fulfilled" or "rejected", with a few convenient callback hooks to set that state.
When a Promise is instantiated it's state is initially "pending" and its constructor takes one argument, which must be a function. That function is immediately executed, and given two arguments, also functions, the first of which must be called to set the state of the promise to "fulfilled" and the second of which must be called to set the state of the promise to "rejected". In a sense the argument to the constructor of a promise is the promised function - it is run, and to let the world know it succeeded it is asked to call the function it received as first argument, and to let the world know it failed it is asked to call the function provided as its second argument, and if it calls neither well ... we'll get to that (but no prize if you guess: the promise simply remains "pending").
To illustrate, a few examples are helpful.
A promise that is immediately fulfilled (which isn't wildly useful) is created with:
new Promise(resolve => resolve());
That is the function just calls the first argument to flag the promise as "fulfilled". Note that this also uses JavaScript's arrow notation for functions. It can also be written any number of different ways using other notations (JavaScript is oh so flexible in that space it seems). Here are a few:
new Promise(function(x) {x()});
- note the name of the first argument is irrelevant. It's the promised function's argument, and can be called whatever you like. All that is important is that the promised function knows this argument is itself a function that it must call to tell the world that it has delivered on the promise - that it is done.
function deliver_me_my_promise(set_state_to_fulfilled) {
set_state_to_fulfilled();
}
new Promise(deliver_me_my_promised);
Which is (literally) more colourful, as now we give the promised function a name, it's no longer anonymous, we've called it, oh so descriptively deliver_me_my_promise()
and its first argument has a revealing name too: set_state_to_fulfilled
. All it does is call set_state_to_fulfilled()
and that indeed is all that happens, the Promise object now has a state of "fulfilled"
Of course, an immediately fulfilling promise isn't so useful. Nothing much happens.
But, wait ...
Enter await
- which is going to make it useful as we will shortly see.
await
is an operator, that waits on a Promise to fulfill (or reject, but let's ignore that for now). And so:
await new Promise(resolve => resolve());
will do nothing, just return. The promise thus created, we just noticed, is fulfilled when it's created and await
checks to see if it's fulfilled and if so, returns. Which it does. This is in fact one of JavaScript's more interesting null statements, the equivalent of a Python pass
.
But what if the promise is pending? That is, it has not yet called either its first or second argument.
In that case await
, true to its name, does not return. It waits patiently (we'll get to how later).
To illustrate though. we could write a promise that is perpetually pending easily enough, by not calling the provided resolver. Here's another useless promise, one that is never fulfilled and always pending:
new Promise(resolve = {})
- as it happens, {}
is the more common form of a JavaScript "do nothing" statement. Hang on, just for a laugh, you probably realised we could write this cute, always pending, promise using the fancier "do nothing" statement we just saw:
new Promise(resolve = await new Promise(resolve = resolve()))
Pointless, and for good humour alone of course to define one promise in terms of another that does nothing.
We could again write this in different ways like:
function deliver_me_my_promise(set_state_to_fulfilled) { }
new Promise(deliver_me_my_promise);
Essentially the promise executor (deliver_me_my_promise()
in this case), the function it runs when it's created, never calls the provided resolver, never tells the Promise that it is "fulfilled" and so it sits there in a perpetual "pending" state.
Now if we await
that:
await new Promise(resolve => {});
await never returns. Just waits forever. This never-fulfilled promise is of course as useless as the immediately-fulfilled promise was. But helps to illustrate what await
does.
As an aside, the means by which it waits is another topic for another day perhaps, but is often described as syntactic sugar around a yield/next structure, which means, in a nutshell only, that:
- the function that calls
await
has its state saved (probably on the heap - where most stuff is stored), - registers this fact with the promise (where it's remembered), returns (to the function calling it) and
- when it is next called it will continue at the line after the
await
.
It is next called as it happens, when the promised function calls either of the functions it received as arguments, the first to fulfill and the second to reject.
These functions are provided by the promise and what they do when called, is set the state of the promise accordingly and call the function that is await
ing (remember that it registered its wait with the promise, and its state was saved so that on next call it continues on the line after the await
). This is the callback hook we referred to earlier, that is when the promised function calls either of its first two arguments, that sets the state of the promise to fulfilled or rejected respectively and calls back any functions that registered themselves with the promise as "waiting".
More useful promises
Your browser (through what's called its Web API) of course provides more useful promises (than our immediately and never fulfilled promises). fetch
is such a useful built-in promise. It fetches a URL, which can take some time, so returns a promise. If you're happy to wait for the URL to return data then await fetch(URL)
is fine and await
will pause until it is no longer pending but fulfilled (or rejected). Of course if you don't want to wait for URL you can attach a callback to the promise using it's .then()
method as in:
fetch(URL).then(call_this_function)
.
That simply registers call_this_function
with the promise, to be called when the promised function calls its first argument. Very much like await
registers the function it's in that way.
Which means that call_this_function
won't be called until JavaScript is idle (aka the stack is empty or all your JavaScript functions have finished). Only then does the event loop look at this queue and call the next thing in it (pulling it off the queue).
The most useful promise (to us here)
We have come full loop, back to setTimeout()
. This is a native JavaScript function places a(nother) function call on the end of the event loop's queue and so this:
await new Promise(first_argument => setTimeout(first_argument, 0));
creates a Promise that runs setTimeout(first_argument, 0)
which places a call to first_argument()
on the end of the event loop's queue.
first_argument()
sets the state of the promise to "fulfilled" and calls any functions that registered themselves with the promise earlier.
await
does just that, registers itself with the Promise requesting a call back to the same line and it registers that interest, perforce, before first_argument()
is called, because the call to first_argument()
is at the end of the event queue which JavaScript only starts processing now that we have given up control with await
.
So while JavaScript is running, await
registered it's desire to be called back when first_argument()
is called, and first_argument()
is called after all JavaScript has finished running, and all events queued ahead of the first_argument()
call on the event loop have been called (and completed).
On the event loop, it's first-in best-dressed, and any UI events that were queued while JavaScript was running are dealt with (browser updates the rendered DOM) and then when they are done, the call to first_argument()
(that setTimeout()
put there) eventually runs and we carry on where we left off and the UI events have been dealt with.
Of course we tend to name that first argument resolve
to produce: await new Promise(resolve => setTimeout(resolve, 0));
. It doesn't matter what it's called, the promise just provides a call back function as the first argument and the name is a conventional mnemonic to remind us this is the function to call, to let the promise know it is fulfilled and to call any registered callbacks.
To recap:
- When the
Promise
is instantiated it runs the function provided as its only argument - the nameless functionfunction noname(resolve) { setTimeout(resolve, 0) }
immediately. -
setTimeout
then puts a call toresolve()
on the end of a event loop queue, behind any waiting UI events. -
await
pauses untilresolve
is called, which does not happen until after all the UI events that were queued before it are called (and handled)
More on pausing
But what does "pausing" mean here? This is a curio worth understanding too, because the event loop queue is not processed until JavaScript is done. So how does it come to be done if it's paused?
The trick is that await
returns, it doesn't pause at all, that is a misnomer. It does save the state of the function it's in first and registers a callback to that state with a promise, but after that it returns. That is the await
statement is a fancy variant of the return
statement. When you execute await
you are practically executing a dressed variant of return
.
The JavaScript continues executing in the function that called the one that the await
was in.
To make clear what is happening here, a firm rule exists in JavaScript, that a function that uses await
must be marked async
. In a nutshell this is illegal:
function myfunc() {
await new Promise(resolve => setTimeout(resolve, 0));
return "I'm done";
}
we are obliged to write it so:
async function myfunc() {
await new Promise(resolve => setTimeout(resolve, 0));
return "I'm done";
}
In no small part, this is intended to remind anyone who calls this function that it may not be finished when it returns ...
Forsooth, guess what? function myfunc()
returns a string (or it would if it were legal and we took the await
out), but async function myfunc()
returns a promise for the string and that promise is still "pending" if await
is called, only when myfunc()
returns "I'm done" is the promise marked "fulfilled" (and and registered call backs are called).
And so if you're content with a promise you can call myfunc()
and it will return a promise. But if you need a string, you can call await myfunc()
and you can see in this manner that await
begets await
and async
is a bit catchy, like a cold ... once a function uses await
and hence must be async
, slowly most functions that rely on it become async
as well, if they need results, otherwise a chain of promises is returned which is is fine too, if all you need is a promise. He who awaits
gets the results of the promise!
But we were wondering how await
pauses. And it should be clear that it doesn't nor does it have to, JavaScript will still run to completion even though it is seemingly paused.
It is not paused, it has saved its state, registered a callback with a Promise (just an object on the heap), which the browser has access to as well. But in so doing it returns to its calling function. That function can either:
- also
await
this function, in which case the same applies (recursively up all the calling functions until eventually JavaScript runs to completion, the awaits all having returned). - not
await
, be a normal function, which just runs to completion.
Then again ...
Rather than await
it is often useful to register explicit callbacks with .then()
.
In the above example we could await myfunc()
or myfunc().then(call_this_function)
. Both register a call back with the promise. The await
registers a call back to the same function (state preserved). The .then()
registers a call back to call_this_function
(any function we name).
Either way, both return and JavaScript runs to completion. it is when a browser event calls resolve()
(the first argument supplied to the promised function) that the promise's state is updated to "fulfilled" and the promise honours the call back requests registered with it (either back to the await
statement or the function registered with .then()
)
And setTimeout()
is the means by which we place a call to resolve()
on the browser's to-do list!
Wrapping up
But back to our cause which is deferring to the UI. We have a solution and we know how it works now.
function defer_to_UI(how_long = 0) {
return new Promise(resolve => setTimeout(resolve, how_long));
}
And in our time-consuming (UI locking) style in-lining function, from time to time we can await defer_to_UI()
.
The function of the await
being only to save the state of our function, register a desire to be called back to this same line, and return. So when we are called back we continue on the next line like nothing happened ... except of course that we released control (returned) and offered JavaScript the chance to process tasks on the event queue (the UI responds).
From time to time, but ...
How often?
If we look at my most extreme example of about 100,000 elements with styles to in-line taking about 30 seconds to do, if we defer to the UI after each element is processed it takes about 11 minutes to complete! Wow! And that is with no UI interactions. That, apparently, is the mere overhead of re-queuing ourselves 100,000 times.
So clearly we don't want to do that. Instead we'd defer_to_UI
at some lower frequency, but what frequency? Either way, to get a feel for things, we should add a progress bar to the style in-lining function.
Which we will look at in the next article ...
Top comments (0)