Best Async Document for Typescript
DRAFT: This document is not complete, until you agree the title of this document is accurate.
JavaScript Engine & Runtime Environment
Javascript code runs in a JavaScript engine provided by the runtime environment.
Runtime Environment | JavaScript Engine |
---|---|
Node.js | V8 |
Deno | V8 |
Bun | JavaScriptCore |
Firefox | SpiderMonkey |
Chrome | V8 |
Edge | V8 |
Safari | JavaScriptCore |
IE | ChakraCore |
Oracle | Nashorn(deprecated) |
In other words, runtime environment (Chrome, Node.js), provides the JavaScript engine (V8). The JavaScript engine provides a single threaded event loop (*) (**).
The JavaScript engine recognizes the functions of the Web/System APIs and hands them off to be handled by the runtime environment. Once those system functions (macro tasks like network, disk, DNS) are finished by the runtime environment, they return and are pushed onto the stack as callbacks. This is managed in the the event loop.
With JavaScript everything is a type of task.
console.log()
statement is a taskconst response = await axios.get(yourUrl)
is a tasksetTimeout(function, 100)
is a taskconst file = readFileSync('./filename.txt', 'utf-8');
is a task
Event loop is made of several queues. Tasks, like above, are grouped into different categories and are put into one of the event / task queues.
A special queue is the PromiseJobs
queue where the promises created by the developer are processed. This queue is also called “microtask queue”. Practically everything else falls into “macrotasks queue”. Immediately after every macrotask, the engine executes all tasks from microtask queue, prior to running any other macrotasks or rendering (on the browser) or anything else.(*)(**).
Node.js is a runtime environment, doesn’t only run JavaScript but utilize multiple threads(for disk I/O etc). It’s the Node.js event loop which is single threaded. Whenever you do something like IO which is event based, it creates a new thread or utilizes an existing thread from the thread pool for it.
Chrome and Node.js share the same V8 JavaScript engine to run JavaScript but have different Event Loop implementations. The Chrome browser uses libevent as its event loop implementation, and Node.js uses libuv.
Let’s keep these statements in mind.
- Blocking methods execute synchronously and non-blocking methods execute asynchronously.
- JavaScript runs in a single thread, but the engine that runs JavaScript is not single threaded.
A Function
Let’s start with a simple function that prints the value provided.
|
|
Output
|
|
This is a blocking function and executes synchronously. When it runs nothing else will be executed until it finishes. If it would take 10 minutes to run this function, everything else after this code block would have to wait for 10 minutes.
Let’s print a number of messages.
|
|
Output
|
|
Pretty logical. Each log()
function executed sequentially one after another.
Arrow Functions
Before we move forward let’s convert log()
function into an arrow function.
|
|
Output
|
|
Same behavior, we just used the arrow notation.
log()
is a synchronous (arrow) function.
A Synchronous Function Which Waits
Let’s create a function that takes a while to run.
We have two different cases here:
- A function that is cpu bound: a function that calculates a big number
- A function that is waiting on external resources: disk IO, network IO etc.
CPU Bound Function
A CPU bound function, only needs CPU cycles to complete. The freeze()
function below, waits for ms
milliseconds and blocks the main loop. After ms
milliseconds it finishes the execution.
|
|
Let’s use freeze()
function.
|
|
Output
|
|
Everything above is as expected. JavaScript engine printed Start
, then the time it took to run, then Finish
.
If we had two tasks each taking 2 seconds, overall the program would run for 4 seconds.
|
|
Output
|
|
setTimeout() Method
Executes a specified block of code once after a specified time has elapsed. The timer is provided by the runtime environment, not by the JavaScript Engine. (Example: Node.js implements setTimeout() method, not V8.)
setTimeout(function[, delay, arg1, arg2, …])
function A function to be executed after the timer expires.
delay (Optional) The time, in milliseconds (thousandths of a second), the timer should wait before the specified function or code is executed. If this parameter is omitted, a value of 0 is used, meaning execute “immediately”, or more accurately, the next event cycle. Note that in either case, the actual delay may be longer than intended; see Reasons for delays longer than specified below.
arg1, …, argN (Optional) Additional arguments which are passed through to the function specified by function.
return value The returned timeoutID is a positive integer value which identifies the timer created by the call to setTimeout(); this value can be passed to clearTimeout() to cancel the timeout.
NOTE: The specified amount of time (or the delay) is not the guaranteed time to execution, but rather the minimum time to execution. The callbacks you pass to these functions cannot run until the stack on the main thread is empty.
As a consequence, code like setTimeout(fn, 0) will execute as soon as the stack is empty, not immediately. If you execute code like setTimeout(fn, 0) but then immediately after run a loop that takes 10 seconds to complete, your callback will be executed after 10 seconds.
We already passed the threshold of lecture to code ratio. Let’s write some code to understand.
An anonymous function that writes hello world
after 200 milliseconds.
|
|
Output
|
|
This one is a little strange. Isn’t it? It seems to be out of order.
First we created a function named log()
which prints hello world
. Later we called setTimeout(log, 1000)
. This will wait for 1000 milliseconds and then call log()
function. Remember, setTimeout()
is actually a system call, that delegates the wait operation to Node.js. As soon as we call setTimeout(log, 1000)
, we push log()
to the macrotasks
queue we mentioned earlier. For this reason, code ran sequentially, as if it ignored setTimeout(log, 1000);
. After 1000 milliseconds, log()
function was pushed to the queue as a callback and executed. Finally, it printed Hello World
.
Welcome to the async world of JavaScript.
Any function that sends a code block to a queue is an async function. Event loop is the code that checks every queue one after another and runs one or more tasks (code blocks) in them. TODO: improve wording here
An ugly representation is below; (TODO: Improve the presentation)
|
console.log("Start");
|
const start = Date.now();
|
--------------> push setTimeout(log, 1000); to macrotasks
|
const finish = Date.now();
|
console.log(`Ran for ${finish - start} milliseconds`);
|
console.log("Finish");
|
1000 milliseconds later
|
log();
By the way even if we set timeout value to 0
milliseconds, the code above would follow the exact same order. Let’s see.
|
|
Output
|
|
Because setTimeout()
is delegated to macrotasks
queue, and because only one item from the macrotasks
queue is pulled with every iteration of the loop, Hello World
is printed at the very end.
The Problem
What if we needed to do something after the async function completes?
Let’s say we have two functions. getMessage()
gets a message from the internet and printMessage()
prints the message retrieved by getMessage()
. It takes time for getMessage()
to get the message.
|
|
|
|
Output
|
|
Not good. We printed the message before we received it.
I have an idea, let’s move printMessage()
into getMessage()
|
|
Output
|
|
Yikes. Let’s try again.
|
|
Output
|
|
Yay. It worked. It worked but what if we want to do something different with the retrieved text? Do we need to change the getMessage()
each time?
Callback Functions
Instead of changing the getMessage()
each time, we can pass a function to printMessage()
.
|
|
Output
|
|
We are getting there. const getMessage = (callback: Function) => {
here we are passing a callback function to getMessage()
and calling back that function when we have what we were waiting for. We call the function a callback function because we call it back after we are done in the current function.
A better implementation is below. getMessage(index: number, cb: Function)
accepts an index to know which message to get, and a function to call back after the message with the index number is received.
|
|
Output
|
|
What if we wanted to add a transformation to the text we receive? Let’s convert the text to upper case before we print.
|
|
Not so easy to read, especially the very last line. We are calling getMessage()
and passing upperCase()
function as the callback. Then we are passing printMessage()
as a callback to upperCase()
function.
What if we wanted to receive messages in order we called them. Let’s modify the code a little bit so it will wait longer for messages[0]
than it waits for messages[1]
.
|
|
Output
|
|
We tried to get messages[0]
, which is Hello World
first but instead we got Hello Universe
as the first message. We have to change the way we called the getMessage()
function if we want to be able to get the messages in the order we called them.
|
|
Output
|
|
What if we had one more message to print? Let’s add another message to messages[]
and print all in order.
|
|
Output
|
|
Imagine we have a chain of these async function;
|
|
This takes us to what is know as callback hell. Imagine adding error handling to the code above. It gets really hard to maintain. There are libraries to manage the chain of callbacks but promises emerged as a better option.
WIP
I decided do publish this post before it is complete. I am going to review the code and add the missing sections in the following days. No promises though.
Promises
TODO: explain promises
Syntax
|
|
Async/Await
TODO: explain Async/Await
sources
- https://academind.com/tutorials/callbacks-vs-promises-vs-rxjs-vs-async-awaits/
- https://javascript.info/settimeout-setinterval
- https://www.javascripttutorial.net/javascript-bom/javascript-settimeout
- https://blog.bitsrc.io/javascript-internals-javascript-engine-run-time-environment-settimeout-web-api-eeed263b1617
- blocking vs non-blocking
- event loop video
- two queues, micro vs macro
- macro and micro tasks
- loop demo
- Promises, Async/Await
- Micro and Macro tasks~
- How does JavaScript and JavaScript engine work in the browser and node?