By Sukhjinder Arora

Original text: blog. Bitsrc. IO/understandi… Zero and identity

JavaScript is a single-threaded programming language, which means that only one thing can happen at a time. That is, the JavaScript engine can only process one statement at a time.

Coding in a single-threaded language is easy because you don’t have to worry about concurrency. Of course, you can’t expect time-consuming operations like network requests not to block your main thread.

Imagine a scenario where when you request an interface, the server will take some time to process your request. The main thread is blocked and the page becomes unresponsive. To solve this problem, JavaScript asynchronous programming emerged. Using asynchronous JavaScript programming techniques like Callbacks, Promises, async/await, you can solve long operations like network requests without blocking the main thread.

How does synchronous JavaScript work

Before diving into asynchronous JavaScript, let’s take a look at how synchronous JavaScript code executes in a JavaScript engine. Such as:

const second = () => { console.log('Hello there! ') } const first = () => { console.log('Hi there! ') second() console.log('The End') } first()Copy the code

To better understand how the above code executes, we must first understand two concepts: the execution context and the call stack.

Execution context

As long as the code is running in the JavaScript engine, it must also be running in the execution context.

When function code executes, there is a function execution context; When global code executes, there is a global execution context. Each function has its own execution context.

The call stack

As can be seen from its name, the call stack is a typical stack structure with the characteristics of first in, last out. As code executes, a number of execution contexts are created, and the call stack is used to store these execution contexts.

Because JavaScript is a single-threaded language, it has only one call stack. The first-in, last-out nature of the call stack also means that all items in the stack can only be added or removed from the top.

Now, let’s go back to the code snippet above and try to understand how JavaScript code is executed in the JavaScript engine.

const second = () => { console.log('Hello there! '); } const first = () => { console.log('Hi there! '); second(); console.log('The End'); } first();Copy the code

What’s going on here

When the code starts executing, a global execution context is created and it is pushed onto the call stack (main() instead). When the first() function is called, it is also pushed onto the stack.

Next, console.log(‘Hi there! ‘) is pushed onto the stack, and when it finishes, it pops off the stack. Next, the second() function is called, so second() is pushed onto the stack.

console.log(‘Hello there! ‘) is pushed onto the stack, and is ejected at the end of execution. The second() function is also popped from the stack when it completes execution.

Console. log(‘The End’) is pushed onto The stack. At this point, first() also completes, so it is also popped.

Finally, the entire program completes, and the global execution context (main()) is popped from the stack.

How does asynchronous JavaScript work?

Now we understand the execution context and call stack, and we know how synchronous JavaScript code is executed. Let’s go back to asynchronous JavaScript code.

What is blocking?

Let’s assume that images or network requests are handled synchronously:

const processImage = (image) => {
    /**
    * doing some operations on image
    **/
    console.log('Image processed');
}


const networkRequest = (url) => {
    /**
    * requesting network resource
    **/
    return someData;
}


const greeting = () => {
  	console.log('Hello World');
}


processImage(logo.jpg);
networkRequest('www.somerandomurl.com');
greeting();
Copy the code

Image processing and network requests take time. So when the processImage() function is called, the time it takes depends on the size of the image.

When processImage() is finished, it will be popped from the stack. The networkRequest() function is then called and pushed onto the stack, which also takes some time to complete its execution.

Finally, the greeting() function is called when networkRequest() completes and pops off the stack. However, this function contains only one statement, console.log, which usually executes very quickly. So the greeting() function executes immediately and is returned.

As you can see, we have to wait for longer functions like processImage() and networkRequest() to finish before we can execute the rest of the code. This also means that these functions block the call stack or main thread, which prevents us from performing other operations.

The solution?

The simplest solution is to use asynchronous callbacks. We use asynchronous callbacks to make our code non-blocking:

const networkRequest = () => {
    setTimeout(() => {
      	console.log('Async Code');
    }, 2000);
};

console.log('Hello World');

networkRequest();
Copy the code

Here we use the setTimeout() method to simulate a network request. Note that the setTimeout() method is not part of the JavaScript engine, it is part of the Web APIs (in the browser) and the C/C++ APIs (in Node.js).

To understand how the above code performs, we also need to understand two concepts: event loops and callback queues (also known as task queues or message queues).

Event loops, Web APIs, and message queues/task queues are not part of the JavaScript engine. It is part of the browser JavaScript runtime environment or node.js JavaScript runtime environment. In Node.js, web APIs are replaced with C/C++ APIs.

Now let’s go back to the code above and see how it executes asynchronously.

const networkRequest = () => {
    setTimeout(() => {
      	console.log('Async Code');
    }, 2000);
};

console.log('Hello World');

networkRequest();

console.log('The End');
Copy the code

When the above code is running in the browser, console.log(‘Hello World’) is pushed onto the call stack and popped from the stack when it finishes execution. Next, the networkRequest() function is executed, so it is pushed to the top of the stack.

Next, the setTimeout() function is called, so it is also pushed onto the stack. At this point, the setTimeout() function also takes two parameters: 1) the callback function and 2) the number of milliseconds.

The setTimout() method starts a 2s timer in the web APIs environment. At this point, setTimeout() is removed from the stack. Finally, console.log(‘The End’) is pushed, executed, and popped.

When the 2s timer expires, the callback is pushed to the message queue. However, the callback function does not execute immediately; it waits for the Event loop to push it onto the call stack.

Event loop

The task of the event loop is to look at the call stack and determine whether the call stack is empty. If the current call stack is empty, it looks to see if there are any callback functions waiting to be executed in the message queue.

In the example above, there is only one callback function in the message queue and the call stack is empty. So, the event loop pushes the callback function onto the call stack.

When console.log(‘Async Code’) is pushed onto the stack, executed and ejected from the stack, the callback function is also ejected from the stack, and the program completes execution.

DOM events

Message queues also include callback functions passed from DOM events, such as click events, keyboard events, and so on:

document.querySelector('.btn').addEventListener('click',(event) => {
  	console.log('Button Clicked');
});
Copy the code

In this example, the event listener waits in the Web APIs environment for the actual event (click event in this case) to occur, and when the event occurs, the callback function will be pushed into the message queue and wait to execute.

The event loop checks if the call stack is empty, and if so, pushes the callback function onto the stack and executes.

So far, we’ve seen how asynchronous callbacks and DOM events are executed — all callback functions are stored using message queues, and then callback functions wait to be executed.

ES6 Work queue/microtask queue

ES6 introduced the concept of work queues/microtask queues, which were used for Promises. The difference between a message queue and a work queue is that a work queue has a higher priority than a message queue, which means that a promise task in a work/microtask queue always takes precedence over a callback function in a message queue.

Here’s an example:

console.log('Script start');

setTimeout(() => {
  	console.log('setTimeout');
}, 0);

new Promise((resolve, reject) => {
    resolve('Promise resolved');
})
.then(res => console.log(res))
.catch(err => console.log(err));
    
console.log('Script End');
Copy the code

Output result:

Script start
Script End
Promise resolved
setTimeout
Copy the code

We can see that the promise is executed before setTimeout. This is because the promise response is stored in the microtask queue, which has a higher priority than the message queue.

Let’s look at another example, this time using two promises and two settimeouts:

console.log('Script start');

setTimeout(() => {
  	console.log('setTimeout 1');
}, 0);

setTimeout(() => {
  	console.log('setTimeout 2');
}, 0);

new Promise((resolve, reject) => {
	resolve('Promise 1 resolved');
})
.then(res => console.log(res))
.catch(err => console.log(err));

new Promise((resolve, reject) => {
    resolve('Promise 2 resolved');
})
.then(res => console.log(res))
.catch(err => console.log(err));

console.log('Script End');
Copy the code

The printed result is:

Script start
Script End
Promise 1 resolved
Promise 2 resolved
setTimeout 1
setTimeout 2
Copy the code

We can see that both promises take precedence over setTimeout’s callback function, again because the event loop takes precedence over the tasks in the microtask queue.

On the other hand, while the event loop is executing a task in the microtask queue, another promise is resolved, which is added to the end of the same microtask queue and is also executed in preference to the message queue callback function, no matter how long the callback has been waiting.

console.log('Script start');

setTimeout(() => {
  	console.log('setTimeout');
}, 0);

new Promise((resolve, reject) => {
    resolve('Promise 1 resolved');
})
.then(res => console.log(res));
  
new Promise((resolve, reject) => {
  	resolve('Promise 2 resolved');
  	}).then(res => {
       	console.log(res);
       	return new Promise((resolve, reject) => {
        	resolve('Promise 3 resolved');
       	})
    }).then(res => console.log(res));
     
console.log('Script End');
Copy the code

The printed result is:

Script start
Script End
Promise 1 resolved
Promise 2 resolved
Promise 3 resolved
setTimeout
Copy the code

Therefore, all tasks in the microtask queue will take precedence over tasks in the message queue. That is, the event loop will first empty the tasks in the microtask queue before executing any callback functions in the message queue.

conclusion

We learned how asynchronous JavaScript works and some other concepts. Concepts such as call stack, event loop, message queue/task queue, and work queue/microtask queue make up the JavaScript runtime environment. Understanding these concepts is not necessary for you to be a good JavaScript developer, but it will certainly help you.

The last word

Your “like” will give me a good mood, and it will be even more perfect if I can get a star.