the role of Event Loop, Event Bus, promises and async

Promises. Promises provide an improved way to handle asynchronous operations and manage their status (completed, rejected, pending). An example of using promises to load data asynchronously

function fetchData(url) {
    return new Promise((resolve, reject) => {
        fetch(url)
            .then(response => response.json())
            .then(data => resolve(data))
            .catch(error => reject(error));
    });
}

fetchData('https://api.example.com/data')
    .then(data => console.log('Data loaded successfully:', data))
    .catch(error => console.error('Error loading data:', error));
  1. Async/Await. The async/await keywords provide syntactic sugar for working with promises, making the code more readable. Example of using async/await:

    async function fetchData(url) {
        try {
            let response = await fetch(url);
            let data = await response.json();
            return data;
        } catch (error) {
            throw new Error('Error loading data');
        }
    }
    
    async function loadData() {
        try {
            let data = await fetchData('https://api.example.com/data');
            console.log('Data loaded successfully:', data);
        } catch (error) {
            console.error('Error loading data:', error);
        }
    }
    
    loadData();

What are Event Loop and Event Bus

Event Loop

Event Loop is a mechanism present in JavaScript runtimes such as the browser and Node.js framework that allows for the processing of asynchronous operations and events. It supports a single-threaded code execution model while still ensuring the responsiveness of the application. Event Loop monitors the call stack and event queue.

Example:

console.log('Start');

setTimeout(function () {
    console.log('Timeout 1');
}, 2000);

setTimeout(function () {
    console.log('Timeout 2');
}, 1000);

console.log('End');

This example will print “Start” first, then “End”. However, the functions inside setTimeout will not be executed immediately. They will be added to the event queue after the specified time has elapsed (in this case, after 2 and 1 second, respectively). Once the call stack is free, Event Loop will add functions from the event queue to the stack and they will be executed.

Event Bus

An Event Bus is a mechanism in programming that allows various components or modules to communicate with each other by sending and listening to events. An Event Bus is often used to exchange messages between different parts of an application without explicitly linking those parts to each other.

Example:

// Пример простого Event Bus на основе событий в Node.js

const EventEmitter = require('events');

// Создаем экземпляр Event Bus
const eventBus = new EventEmitter();

// Компонент 1: слушает событие 'message'
eventBus.on('message', (data) => {
    console.log('Component 1 received message:', data);
});

// Компонент 2: слушает событие 'message'
eventBus.on('message', (data) => {
    console.log('Component 2 received message:', data);
});

// Компонент 3: отправляет событие 'message'
function sendMessage() {
    eventBus.emit('message', 'Hello, Event Bus!');
}

// Вызываем функцию отправки сообщения
sendMessage();

In this example, components 1 and 2 listen to the 'message' event, and component 3 sends the event. When sendMessage is called, both components 1 and 2 will receive a notification and print a message to the console. Event Bus allows you to conveniently organize interaction between components, even if they are located in different parts of the application.

For example, Vue.js 2 does not have an explicit Event Bus, but often uses a global Vue instance as a primitive Event Bus.

<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Vue 2 Event Bus Example</title>
</head>
<body>
    <div id="app">
        <child-component></child-component>
        <another-child-component></another-child-component>
    </div>

    <script src="https://cdn.jsdelivr.net/npm/vue@2"></script>
    <script src="https://habr.com/ru/articles/801543/main.js"></script>
</body>
</html>
// main.js
Vue.component('child-component', {
    template: '<div><button @click="sendMessage">Send Message</button></div>',
    methods: {
        sendMessage() {
            this.$root.$emit('message', 'Hello from Child Component');
        }
    }
});

Vue.component('another-child-component', {
    template: '<div>Another Child Component</div>',
    created() {
        this.$root.$on('message', (data) => {
            console.log('Another Child Component received message:', data);
        });
    }
});

new Vue({
    el: '#app',
});

This example creates a global Vue instance (new Vue) in the root element of the application. Component child-component contains a button that, when clicked, sends a message via $emit to the global Event Bus (this.$root). Component another-child-component listens to this event through $on while creating.

The application as a whole acts as an Event Bus, allowing components to communicate with each other by sending and listening to events. In a real project, it's better to use Vuex for state management, but for simple cases, Event Bus can be a handy tool.

Micro and macro tasks

Microtasks and macrotasks are parts of the asynchronous execution model in JavaScript and are associated with mechanisms such as Event Loop.

  1. Macrotasks:

    • Definition: Macro tasks are larger tasks that are added to the Event Loop execution queue.

    • Usage: Tasks such as user input processing, script execution, asynchronous I/O (input/output) operations, timers (setTimeout, setInterval), animation requests and DOM events.

    console.log('Start');
    
    setTimeout(function () {
        console.log('Timeout (Macrotask)');
    }, 0);
    
    console.log('End');

    In this example, the function passed to setTimeout, is a macrotask. It will be executed after the main code is executed, even if the timer is set to 0 milliseconds.

  2. Microtasks:

    • Definition: Microtasks are smaller, faster tasks that are processed at the end of each macrotask in the current call stack.

    • Usage: Promises (then, catch, finally), async/await operator.

    console.log('Start');
    
    Promise.resolve().then(function () {
        console.log('Promise (Microtask)');
    });
    
    console.log('End');

    Here's the function passed to .then() promise is a microtask. It will be executed after the main code and any macro tasks, but before the events in the event queue.

Execution order:

  1. The main code is executed.

  2. Microtasks (if any) from the current call stack are executed.

  3. The macrotask (the first one in the queue) is running.

  4. Repeat steps 2-3 until the macrotask queue is empty.

Promises

Promises are a powerful mechanism in JavaScript for managing asynchronous operations. They are used to handle results or errors that may occur in the future after an asynchronous task has completed. Promises provide a readable and convenient syntax for working with asynchronous operations.

Basic properties and methods of promises

  1. new Promise(executor): Creates a new promise object. executor is a function that takes two arguments: a function resolve and function reject. They are used to complete the promise successfully (resolve) or with an error (reject).

    let promise = new Promise((resolve, reject) => {
        // асинхронная операция
        let success = true;
    
        if (success) {
            resolve('Успех!');
        } else {
            reject('Ошибка!');
        }
    });
  2. promise.then(onFulfilled, onRejected): Method then adds handlers for successful completion (onFulfilled) or errors (onRejected). Each of them is a function that accepts a result or an error respectively.

    promise.then(
        result => console.log(result),
        error => console.error(error)
    );
  3. promise.catch(onRejected): Method catch used for error handling, similar to the second argument in then.

    promise.catch(error => console.error(error));
  4. Promise.all(iterable): Returns a promise that is fulfilled when all promises in the passed array or iterable complete, or fail with the first error.

    let promise1 = Promise.resolve(1);
    let promise2 = new Promise(resolve => setTimeout(() => resolve(2), 1000));
    let promise3 = Promise.reject('Ошибка');
    
    Promise.all([promise1, promise2])
        .then(values => console.log(values))
        .catch(error => console.error(error)); // будет вызвано, если один из промисов отклонится
  5. Promise.race(iterable): Returns a promise that is fulfilled or rejected according to how the first promise in the passed array or iterable ends.

    let promise1 = new Promise(resolve => setTimeout(() => resolve('Winner'), 1000));
    let promise2 = new Promise(resolve => setTimeout(() => resolve('Loser'), 2000));
    
    Promise.race([promise1, promise2])
        .then(winner => console.log(winner)) // 'Winner'
        .catch(error => console.error(error));

An example of using promises

function fetchData(url) {
    return new Promise((resolve, reject) => {
        fetch(url)
            .then(response => response.json())
            .then(data => resolve(data))
            .catch(error => reject(error));
    });
}

fetchData('https://api.example.com/data')
    .then(data => console.log('Data loaded successfully:', data))
    .catch(error => console.error('Error loading data:', error));

This example demonstrates the use of a promise to load data from the server asynchronously. Method fetchData returns a promise that resolves successfully when the data is loaded successfully, and rejects if there is an error. then And catch are used to handle success and error respectively.

Fetch API

The Fetch API is an interface for sending and receiving HTTP requests. It provides a more flexible and powerful way to handle network requests compared to the legacy XMLHttpRequest. The Fetch API is based on promises, which makes it convenient for asynchronous programming.

Main features of Fetch API:

  1. Ease of use. The Fetch API provides a simple and easy to use syntax based on promises. This makes the code more readable and understandable.

    fetch('https://api.example.com/data')
        .then(response => response.json())
        .then(data => console.log(data))
        .catch(error => console.error(error));
  2. Support for headers and methods. The Fetch API allows you to easily manage HTTP request headers and methods.

    fetch('https://api.example.com/data', {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json',
            'Authorization': 'Bearer token123'
        },
        body: JSON.stringify({ key: 'value' })
    })
    .then(response => response.json())
    .then(data => console.log(data))
    .catch(error => console.error(error));
  3. Streams. The Fetch API supports data streaming, which is useful when working with large amounts of data, such as file downloads or streaming reads.

  4. Method Response: An object Responsereturned by the method fetchprovides many methods for working with responses, such as json(), text(), blob()and others.

    fetch('https://api.example.com/data')
        .then(response => {
            if (!response.ok) {
                throw new Error('Network response was not ok');
            }
            return response.json();
        })
        .then(data => console.log(data))
        .catch(error => console.error('Fetch error:', error));
  5. Cancel requests: The Fetch API does not have built-in support for canceling requests, but you can use third-party libraries or create your own cancellation mechanism based on component lifecycle control (in the case of web applications).

Fetch API usage example

// Отправка GET-запроса
fetch('https://api.example.com/data')
    .then(response => response.json())
    .then(data => console.log('Data loaded successfully:', data))
    .catch(error => console.error('Error loading data:', error));

// Отправка POST-запроса с данными
fetch('https://api.example.com/submit', {
    method: 'POST',
    headers: {
        'Content-Type': 'application/json',
        'Authorization': 'Bearer token123'
    },
    body: JSON.stringify({ key: 'value' })
})
.then(response => response.json())
.then(data => console.log('Data submitted successfully:', data))
.catch(error => console.error('Error submitting data:', error));

This example demonstrates sending GET and POST requests using the Fetch API to load and post data to the server.

Syntactic Sugar – Async and Await

Syntactic sugar in programming is a convenient and more readable syntax for performing certain operations. In the context of JavaScript, keywords async And await provide syntactic sugar for working with promises, which makes the code more concise and easier to understand.

async

Keyword async used before a function to indicate that the function always returns a promise. The promise will be resolved with the result of the function execution or rejected with an error.

Example without async:

function fetchData() {
    return new Promise(resolve => {
        setTimeout(() => {
            resolve('Data loaded successfully');
        }, 2000);
    });
}

Example with async:

async function fetchData() {
    return new Promise(resolve => {
        setTimeout(() => {
            resolve('Data loaded successfully');
        }, 2000);
    });
}

await

Keyword await used inside a function declared using async, to wait for the promise to be fulfilled. It pauses the function until the promise is resolved or rejected.

Example without await:

function fetchData() {
    return new Promise(resolve => {
        setTimeout(() => {
            resolve('Data loaded successfully');
        }, 2000);
    });
}

function processData() {
    fetchData().then(data => {
        console.log(data);
    });
}

processData();

Example with await:

async function fetchData() {
    return new Promise(resolve => {
        setTimeout(() => {
            resolve('Data loaded successfully');
        }, 2000);
    });
}

async function processData() {
    const data = await fetchData();
    console.log(data);
}

processData();

Usage await makes the code more linear and similar to synchronous code, making it easier to read and understand.

Example of using async and await together:

async function fetchData() {
    return new Promise(resolve => {
        setTimeout(() => {
            resolve('Data loaded successfully');
        }, 2000);
    });
}

async function processData() {
    try {
        const data = await fetchData();
        console.log(data);
    } catch (error) {
        console.error('Error:', error);
    }
}

processData();

Here we declare an asynchronous function processDatawe use await to wait for the fulfillment of a promise from fetchDataand use the block try/catch to handle possible errors. This makes the code more expressive and easier to handle asynchronous operations.

Similar Posts

Leave a Reply

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