Javascript is a synchronous programming language. But it is also the language of the Web which requires asynchronous fetching of data, also we want user interactivity, namely, functions that are event driven and executed based on user behavior only.
Callbacks and Promises
We achieve this by creating pieces of code that do not execute immediately when the program initializes. Those pieces of code are also known as callbacks and promises. Initially, the most basic approach was with pure callbacks, but the very nature of function definitions lead to the so called 'callback hell' of nested callbacks. Thus, a more refined tool emerged - a promise. Nowadays, we also have the async/await as a popular alternative to the '.then()' resolving of promises. However this article's purpose is not to educate on the basics of dealing with asynchronous requests.
It attempts to delve into the nature of Javascript and explain how come asynchronous tools such as callbacks and promises are part of it.
So how come a single-threaded programming language have asynchronous tools at its disposal?
Javascript runtime
The straightforward answer is - Javascript runtime. Nowadays, Javascript is not only a scripting language with the simple purpose of toggling a button on a static web page, it a fully capable programming language. Thus, to understand how a single-threaded language can achieve concurrency, let's start with the following image.
We see a JS engine and a few additional features - Web API, Event loop and a Queue. When we say single thread, we refer to the JS engine. Simply said, this relates to the code you have written and its execution contexts, while the additional features 'create' an additional thread. So, the runtime consists of two parts - your code and some other external code both producing the whole application. So, modern Javascript always needs a runtime environment to execute and the most popular one is the browser. Another popular one is Node.js.
But now, let's look at the mechanism of a callback function, applying what we have learned about runtime environment already.
Callbacks
function fetchData(callback) {
setTimeout(function () {
const data = 'Async data';
callback(data); // Execute the callback function with the data
}, 1000);
}
function processData(data) {
console.log('Received data: ', data);
}
fetchData(processData); // Pass the processData function as a callback
console.log('Fetching data...');
The Line of Execution:
console.log('Fetching data...'); // This line is executed first.
console.log('Received data: Async data'); // This line is executed second.
A Step by Step Explanation:
- fetchData(processData) is triggered first
- setTimeout is called with a delay of 1000 milliseconds.
- While waiting for the timeout, the program continues by executing console.log('Fetching data...');
- After 1000 milliseconds (1 second), the callback function processData is executed with the data 'Async data'.
- Inside processData, the statement console.log('Received data:', data); is finally executed.
So, the final order of printing statements is:
- "Fetching data..."
- "Received data: Async data"
Thus, the "Fetching data..." console.log executes before the fetchData function.
How is this possible?
How does Javascript, which is single-threaded, 'know' to continue interpreting the code and execute the subsequent console.log("Fetching data..."), even though it has another task seemingly on standby (fetchData function).
As I said, when we say single-threaded, we refer to the execution mechanism of the JS engine. The Javascript engine is responsible for the execution contexts, namely, managing the memory heap and the call stack. The memory heap stores all the variables defined in our JS code while the call stack performs the operations (function execution).
So, back to the current problem, single-threaded means only one call stack. One call stack, in turn, means only one piece of code can be executed at a time.
In our case with console.log('Fetching data...') and fetchData function, given the nature of Javascript to be non-blocking, Javascript does not wait for the response of the callback, but moves on with the interpretation of the subsequent blocks of code.
Why does it not wait?
The answer - the callback fetchData function is 'extracted' from the call stack of the current main thread and logically the execution continues with console.log('Fetching data...').
But where is this callback 'extracted' to?
The general answer not only for callbacks is that any such asynchronous function utilizes the Web API by relying on the Event loop to manage its queue priority when to re-enter the main thread call stack.
In the case of callbacks, the Timer Web API executes the setTimeout and the Event loop puts the result of that function in the Tasks Queue. That is also why the delay set in setTimeout is known as a minimum delay time. It is unclear when exactly the call stack will be freed up so that a new event cycle can be executed and tasks from the queue be added to it.
Let's now look at how promises are resolved in the Javascript runtime.
Promises
const promise = new Promise(resolve=>{
resolve("Promise")
}, reject => {
})
promise.then(res=>console.log(res))
In the case of promises, the promise is set into the Microtask Queue which the event loop ranks with higher priority than regular tasks such as setTimeout callbacks. Meaning when/if a promise is fulfilled, its result is added to the microtask queue, ensuring that it will be executed before the next (regular) task in the event loop.
Inversion of control
Thus, with simple words, the Javascript runtime adds threads and concurrency to the otherwise, single threaded programming language.
That would also be known as inversion of control, a very popular term in programming. The code result becomes dependant on an external factor, namely, the additional features that come with every Javascript runtime. Control of execution is inverted and handed over to an external entity as described above. In
Top comments (0)