Event Loop Basics in JavaScript

This way, long-running operations, such as server requests or timers, do not prevent code from continuing to execute. It is thanks to the Event Loop that the application can remain responsive, allowing users to continue interacting with the interface while heavy tasks are performed in the background.

Some may have the illusion of parallelism when in fact only one task is running at any given time.

In this article, we'll look at how to implement Event Loop in JavaScript.

Macrotasks and microtasks

Microtasks

Microtasks are tasks that must be executed immediately after the currently executed script and before the Event Loop continues to process macro tasks.

The main point here is – priority of microtasks. After each macrotask completes, JS will first process all microtasks in the queue before moving on to the next macrotask (more on them below).

Examples:

Promises – the most common version of micro-tasks. When a promise goes into state “done” (fulfilled) or “rejected” (rejected), corresponding handlers .then() or .catch() are added to the microtask queue.

console.log('Начало');

Promise.resolve().then(() => {
    console.log('Обработка промиса');
});

console.log('Конец');

Even though the promise is resolved immediately, the text Обработка промиса will be displayed after Конецbecause the handler .then() waits in the microtask queue until the current script completes.

async/await are also based on microtasks. When a function is declared as asyncit automatically returns a promise. await causes JS to wait for the promise to resolve, adding the rest of the function as a microtask:

async function asyncFunction() {
    console.log('Внутри async функции');
    await Promise.resolve();
    console.log('После await');
}

console.log('Перед вызовом async функции');
asyncFunction();
console.log('После вызова async функции');

После await is displayed after После вызова async функции.

HTML has a function queueMicrotaskwhich allows you to place functions in a microtask queue:

console.log('Перед queueMicrotask');

queueMicrotask(() => {
    console.log('Внутри микрозадачи');
});

console.log('После queueMicrotask');

Внутри микрозадачи will be displayed after После queueMicrotask.

Macro tasks

Macro tasks are tasks which are planned for future implementation. They will be added to the end of the event queue and will only be processed after all microtasks have been completed.

Examples:

setTimeout allows you to postpone the execution of a function for a certain period of time:

console.log('Начало');

setTimeout(() => {
    console.log('Выполнение через setTimeout');
}, 1000);

console.log('Конец');

Message Выполнение через setTimeout will be displayed after Конецeven if the delay is only 1 millisecond, because setTimeout always places the call in the macrotask queue, which will be processed after all current microtasks have completed.

setInterval look like setTimeoutbut allows the function to be executed regularly at a given time interval:

console.log('Начало интервального выполнения');

let count = 0;
const intervalId = setInterval(() => {
    console.log('Интервал');
    count++;
    if (count === 5) {
        console.log('Остановка интервала');
        clearInterval(intervalId);
    }
}, 500);

console.log('Код после установки интервала');

The code will display a message regularly Интервал every 500 milliseconds until the counter reaches 5, at which point the interval will stop.

You can load external scripts through the element <script> with attribute src. In this case, script execution will only begin after the script has been fully loaded, and this will happen asynchronously with respect to the rest of the page:

<script>
  console.log('Перед загрузкой скрипта');
</script>
<script src="https://habr.com/ru/companies/otus/articles/801249/path/to/external/script.js"></script>
<script>
  console.log('После загрузки скрипта');
</script>

The main difference between micro and macro tasks is their priority and the way the Event Loop processes them. Microtasks are processed immediately after the current script and before any macrotasks, ensuring that promises and other critical operations are completed quickly. Macrotasks, on the other hand, provide a way to schedule tasks to be completed in the future.

Web API

A Web API is a set of asynchronous APIs provided by a runtime environment (such as a browser) that allows you to perform tasks such as manipulating the DOM, sending AJAX requests, setting timers, and more. These APIs are not part of JS, but they can be called from JavaScript.

When JS code calls an asynchronous Web API (for example, fetch for an AJAX request or setTimeout), the request is sent to the corresponding Web API module, and JavaScript itself continues to execute without blocking.

The Web API takes care of executing the request. For example, if this is an AJAX request, the Web API handles the entire network communication process. For a timer, the Web API will track the time it takes for it to fire.

When the Web API has completed its work (for example, a response to an AJAX request has been received or it is time to setTimeout), the callback function associated with this asynchronous call is placed in the event queue.

Event Loop regularly checks the event queue for tasks ready to be executed. If the JavaScript call stack is empty, Event Loop pops events (callback functions) from the queue and pushes them onto the call stack for execution.

Examples:

Fetch API has a good and flexible interface for executing AJAX requests. This is a promise-based way to request resources asynchronously:

console.log('Начало выполнения скрипта');

fetch('https://api.example.com/data')
  .then(response => response.json())
  .then(data => console.log(data))
  .catch(error => console.error('Ошибка при выполнении запроса:', error));

console.log('Конец выполнения скрипта');

fetch() makes an HTTP request to the specified URL and then processes the resulting response. We use the chain .then() to convert the response to JSON format and to process the data. Last .catch() intercepts possible request errors. It is worth noting that the console output 'Конец выполнения скрипта' will appear before the data or error from fetch().

Changing the DOM is common in JS development and also interacts with the Web API. Let's say there is a task to update the contents of an element upon completion of an asynchronous operation:

console.log('Начало скрипта');

setTimeout(() => {
  document.getElementById('myElement').textContent="Обновленное содержимое";
  console.log('Содержимое элемента обновлено');
}, 2000);

console.log('Конец скрипта');

setTimeout() used to simulate delay – for example, waiting for a response from the server. After a delay of 2 seconds, the element's contents are updated.

Web Workers allow you to perform complex calculations in a background thread without blocking the main execution thread:

if (window.Worker) {
  const myWorker = new Worker('worker.js');

  myWorker.postMessage('Начать обработку');

  myWorker.onmessage = function(e) {
    console.log('Сообщение от worker:', e.data);
  };
} else {
  console.log('Web Workers не поддерживаются в вашем браузере.');
}

We create a new web worker that runs in worker.js. We send a message to the worker to start processing and install a handler to receive the result of its work.

QueueMicrotask

queueMicrotask as a function makes it possible to place tasks in a microtask queue.

Let me remind you that microtasks (i.e., they have the highest priority) are executed immediately after the current script, but before the browser has the opportunity to redraw the page or process any events.

Examples:

Asynchronous error handling:

function asyncOperationWithErrorHandling() {
    try {
        // предположим, здесь может произойти ошибка
        throw new Error('Что-то пошло не так');
    } catch (error) {
        // планируем асинхронную обработку ошибки
        queueMicrotask(() => console.error('Асинхронно обработанная ошибка:', error));
    }
}

asyncOperationWithErrorHandling();

The error is caught in the block try...catchand its processing is scheduled asynchronously using queueMicrotask

Controlling the execution order of asynchronous code:

console.log('Начало скрипта');

queueMicrotask(() => console.log('Выполнение микрозадачи'));

Promise.resolve().then(() => console.log('Обработка промиса'));

console.log('Конец скрипта');

A task added using queueMicrotaskwill be executed before the browser has a chance to process other macro tasks, but after the current script and tasks scheduled using promises.

Guaranteed code execution after all promises:

Promise.resolve().then(() => console.log('Промис 1 выполнен'));
Promise.resolve().then(() => console.log('Промис 2 выполнен'));

queueMicrotask(() => console.log('Гарантированное выполнение после всех промисов'));

One common mistake is to assume that asynchronous operations, e.g. setTimeout with a delay of 0 ms, will be executed immediately after the current block of code. In fact, such operations will be placed in a task queue and executed only after the call stack has been cleared and the Event Loop can handle them.

It is also important to remember that await pauses the execution of a function until the promise is resolved, which can block subsequent code from executing if you do not pay attention to the sequence of asynchronous operations.

Overall, proper use of Event Loop greatly helps in asynchronous JavaScript development.

The article was prepared in anticipation of the start online course “JavaScript Developer. Professional”

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *