Write it before the article

This article is a translation from Sukhjinder Arora’s Understanding Asynchronous JavaScript. This article describes how asynchronous and synchronous JavaScript works in the runtime environment, using call stacks, message queues, job queues, and event loops. Please forgive me if there is any bad translation in the article.

Understanding asynchronous JavaScript

JavaScript is known to be a single-threaded programming language, which means that only one thing can happen at a time. In layman’s terms, a JavaScript engine can only handle one declaration per thread at a time.

While single-threaded languages make it easier to write code because you don’t have to worry about concurrency, it also means you can’t do long-running operations like network access without locking the main thread.

Imagine requesting data from an API. The server may take some time to process the request, blocking the main thread and rendering the page unresponsive.

This is where asynchronous Javascript comes in handy. With asynchronous JavaScript (such as callbacks, promises, and async/await), you can perform long network requests without locking the main thread.

You don’t need to learn all these concepts to be a great JavaScript engineer, they just help you 🙂

So without further ado, let’s get started.

How does synchronous JavaScript work?

Before we dive into asynchronous JavaScript, let’s take a look at how synchronous JavaScript code is executed inside the engine. Here’s an example:

const second = () => {
   console.log('hello there');
}

const first = () => {
    console.log('hi,there');
    second();
    console.log('The End');
}

first();
Copy the code

Before we can understand how the above code executes in the JavaScript engine, we need to understand the concept of execution context and call stack (also known as execution stack).

Execution context

An execution context is an abstraction of where JavaScript code is evaluated and executed. Whenever any JS code executes, they run inside the execution context.

Functions execute in the execution context of functions, and global code executes in the global execution context. Each function has its own execution context.

The call stack

The call stack, as its name suggests, is a lifO stack structure that stores all execution contexts created during code execution.

JavaScript has a single call stack because it is a single-threaded language. The LIFO structure of the call stack determines that things can only be added or removed from the top of the stack.

Let’s go back to the above snippet and try to understand how it is executed inside the JavaScript engine.

 const second = () => {
   console.log('hello there');
}

const first = () => {
    console.log('hi,there');
    second();
    console.log('The End');
}

first();
Copy the code

Call stack for the code above:

So what’s going on?

When the code executes, a global execution context is created (represented by main()) and pushed to the top of the call stack. When first() is called, first() is pushed to the top of the call stack.

Next, console.log(‘hi,there’) is pushed to the top of the stack, and when it finishes, it pops out of the stack. After that, we call second(), so second() is pushed to the top of the stack.

console.log(‘Hello there! ‘) is pushed to the top of the stack and is popped when it finishes executing. At this point, second() is finished, so it is popped off the stack.

Console. log(‘The End’) is pushed to The top of The stack and then removed at The End. The first() function then completes execution and is removed from the call stack.

At this point, the entire program ends the call, so the global execution context (main()) pops out of the stack.

How does asynchronous JavaScript work?

Now that we have an overview of the call stack and how synchronous JavaScript works, let’s return to asynchronous JavaScript.

What is a lock?

Let’s imagine that we are using synchronous methods for image processing or network requests. Such as:

Const processImage = (image) => {// processImage console.log('Image Processed'); } const netWorkRequest = (url) => {// Network resource requestreturn 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 processImage() is called, the time it takes will depend on the size of the image.

When the processImage() function terminates, it is removed from the call stack. The networkRequest() function is then called and pushed onto the stack. So again, it takes some time to end the call.

Finally, when networkRequest() ends, the greeting() function is called. Because it contains only a console.log declaration, and the console.log declaration is a special block to execute, the greeting() function is called quickly.

As you can see, we have to wait until functions (like processImage() and networkRequest()) finish executing. This means that these functions are locked in the call stack or main thread. So we can’t do anything else during the above code execution, which is definitely not what we want.

So what’s the solution?

The simplest solution is asynchronous callback. We use asynchronous callbacks to keep our code from being locked. Here’s an example:

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

networkRequest();
Copy the code

Here I use the setTimeout method to simulate a network request. Note that setTimeout is not part of the Javascript engine, it is part of the Web Api(in browsers) and C/C++ (in Node.js).

To understand how this code performs, we need to understand more concepts such as event loops and callback queues (also known as task queues or message queues).

Event loops, WEB apis, message queues/task queues are not part of the JavaScript engine, they are part of the browser’s JavaScript runtime environment or the Node.js JavaScript runtime environment. In Nodejs, the network interface is replaced by C/C++ apis.

Now, let’s go back to the code above and take a look at how they execute 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 loaded in the browser, console.log(‘Hello World’) is pushed and pushed when the call ends. Next, networkRequest() is called, so it is pushed to the top of the stack.

The setTimeout() method is then called, so it is pushed to the top of the stack. The setTimeout function takes two arguments: 1) the callback function and 2) the time in ms. SetTimeout starts a timer for 2 seconds in the Web API environment. At this point, setTimeout is over, so it is ejected from The stack, and console.log(‘The End’) is then pushed onto The stack, executed, and removed from The stack after completion.

Meanwhile, the timer runs out, and now the callback is pushed into the message queue, but instead of being executed immediately, it is placed where the event loop begins.

Event loop

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

In this example, the message queue contains a callback function, and the call stack is empty. So the event loop pushes the callback function to the top of the stack.

After that, the console.log(‘ Async Code ‘) statement is pushed to the top of the stack, executed, and popped off the stack. At this point the callback function ends, so it is popped off the stack, and the whole program finishes.

DOM events

Message queues also include callback functions for DOM events such as click events and keyboard events, for example:

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

In DOM events, event listeners sit in the Web API environment waiting for an event to occur (in this case, the click event), and when that event occurs, the callback function is placed in the message queue waiting to be executed.

The event loop checks again to see if the call stack is empty, if so, it pushes the event callback onto the stack, and the callback function is executed.

We’ve seen how asynchronous callbacks and DOM events are executed, using message queues to store all callbacks waiting to be executed.

ES6 Job queue/microtask queue

ES6 introduces a concept called Job queue/microtask queue that Promises to be used in JavaScript. The difference between a message queue and a job queue is that the job queue has a higher priority than the message queue, meaning that the Promise task in the job queue/microtask queue executes before the callback function in the message queue.

Such as:

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:

Script start
Script End
Promise resolved
setTimeout
Copy the code

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

Let’s look at the next example. This time there are 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

This time output:

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

We can see that both promises are executed before the setTimeout callback, because in the event loop mechanism, tasks in the microtask queue take precedence over tasks in the message queue/task queue.

If another Promise is in resolved state while the event loop is executing the task in the microtask queue, it will be added to the end of the same microtask queue and will execute before the callback in the message queue, regardless of how long the callback has been waiting to execute. Having a high priority means being able to do whatever you want.

Here’s an example:

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

Output this time:

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

So all tasks in the microtask queue will be executed before tasks in the message queue. That is, the event loop will empty the tasks in the microtask queue before performing any callbacks to the message queue.

conclusion

We’ve seen how asynchronous JavaScript works, as well as other concepts such as call stacks, event loops, message/task queues, and work/micro-task queues, which together make up the JavaScript environment. Again, you don’t need to learn all of these concepts to be a great JavaScript developer, but it helps to know them 🙂

That’s all for today’s post. If you find this post helpful, please click the applause button next to it. You can also follow me on Medium and Twitter. If you have any questions, feel free to leave a comment below and I will be happy to help you 🙂

Conclusion the translator

If you have any comments or suggestions on my translation or content, please leave a comment below and let me know. If you like this article, give a thumbs up. Thank you very much for reading it