Event loop для чайников: простыми словами о сложном механизме браузера

Event Loop — это сердце асинхронности в JavaScript. В этой статье простыми словами разберем, как работает цикл событий в браузере, что такое макрозадачи и микрозадачи, и как они влияют на выполнение кода. С примерами, схемами и лайфхаками для лучшего понимания. Идеально для тех, кто хочет разобраться в сложном механизме без лишней воды!

314 открытий2К показов
Event loop для чайников: простыми словами о сложном механизме браузера

Салют, народ! 👋

Это моя первая проба пера, так что не судите строго. Сегодня мы разберем одну из самых интересных и, на первый взгляд, сложных тем в JavaScript — Event Loop (или цикл событий). Если вы когда-нибудь задумывались, как браузер умудряется выполнять множество задач одновременно, не зависая при этом, то вы попали по адресу.

Event Loop — механизм, который управляет асинхронными операциями в JavaScript. Он позволяет обрабатывать задачи, не блокируя основной поток выполнения программы. Это особенно важно для создания отзывчивых интерфейсов пользователя и эффективной работы серверов.

Интересно, что Event Loop работает как в браузере, так и в Node.js, но с некоторыми отличиями. В этой статье мы сосредоточимся на том, как это происходит в браузере, и постараемся объяснить всё максимально просто, даже если вы только начинаете свой путь в программировании.

Поехали! 🚀

Как работает Event Loop и из чего состоит

Структура Event Loop

Основная цель Event Loop — циклическое выполнение задач, которые делятся на две категории:

  1. Синхронные задачи — выполняются немедленно;
  2. Асинхронные задачи — добавляются в очередь и выполняются позже.

Макрозадачи и микрозадачи

Одним из ключевых аспектов работы event loop выступают макрозадачи (macrotasks) и микрозадачи (microtasks). Они представляют собой две очереди задач, которые имеют различный приоритет выполнения.

  • Макрозадачи. Выполняются после завершения текущей синхронной задачи. Примеры:Синхронный код; таймеры (setTimeoutsetInterval); события пользовательского интерфейса (например, клики мыши); сетевые запросы (например, fetch или XMLHttpRequest).
  • Микрозадачи. Выполняются сразу после завершения текущей синхронной задачи, но перед началом следующей макрозадачи. Примеры:Промисы (Promise.resolve()Promise.reject()).MutationObserver.async/await).

В Node.js также есть event loop, но с небольшими различиями. Например, там используются дополнительные очереди для работы с файловой системой и сетью.

Схема приоритетов выполнения:

  1. Сначала выполняется текущий синхронный код;
  2. Затем выполняются все микрозадачи в очереди Microtask queue до тех пор, пока очередь не станет пустой;
  3. После этого выполняется следующая макрозадача из очереди Macrotask queue и т.д.

Как поведет себя Event Loop в реальной жизни: разбираем код

Схемы это конечно хорошо, но всегда лучше изучать на примерах кода. Далее посмотрим куски, которые могут встретиться во время работы или на собеседованиях.

Вложенные промисы и микрозадачи

			setTimeout(() => {
    console.log('Timeout 1');
    Promise.resolve().then(() => {
        console.log('Promise in timeout');
    });
}, 0);

Promise.resolve().then(() => {
    console.log('First promise');
    Promise.resolve().then(() => {
        console.log('Second promise');
    });
});

console.log('Start');
		

Порядок вывода:

  1. Start
  2. First promise
  3. Second promise
  4. Timeout 1
  5. Promise in timeout

Объяснение:

  • Сначала выполняется синхронный код (console.log('Start')).
  • Затем выполняются микрозадачи (микротаски) — это цепочки промисов.
  • После завершения всех микрозадач выполняются макрозадачи (например, setTimeout).

Асинхронные функции и генераторы

			function* generator() {
    yield new Promise(resolve => {
        setTimeout(() => {
            console.log('Generator timeout');
            resolve();
        }, 0);
    });
}

async function asyncFunction() {
    console.log('Async start');
    await generator().next().value;
    console.log('After generator');
}

asyncFunction();

console.log('Main thread');
		

Порядок вывода:

  1. Async start
  2. Main thread
  3. Generator timeout
  4. After generator

Объяснение:

  • Функция asyncFunction начинает выполнение, но останавливается на await, пока не завершится промис, созданный генератором.
  • Синхронный код продолжает выполняться (console.log('Main thread')).
  • Когда таймер завершается, event loop обрабатывает завершение промиса и продолжает выполнение asyncFunction.

Взаимодействие с DOM и таймерами

			document.getElementById('button').addEventListener('click', () => {
    console.log('Button clicked');
    setTimeout(() => {
        console.log('Timeout after click');
    }, 0);
});

setTimeout(() => {
    console.log('Initial timeout');
    document.getElementById('button').click();
}, 0);
		

Порядок вывода:

  1. Initial timeout
  2. Button clicked
  3. Timeout after click

Объяснение:

  • Первый таймер добавляет событие клика на кнопку.
  • После выполнения первого таймера происходит имитация клика на кнопку.
  • Event loop обрабатывает событие клика и устанавливает второй таймер.
  • После завершения всех синхронных задач выполняются все установленные таймеры.

Рекурсивные промисы и рекурсия

			function recursivePromise(n) {
    if (n <= 0) return Promise.resolve();
    return new Promise(resolve => {
        setTimeout(() => {
            console.log(`Recursive ${n}`);
            recursivePromise(n - 1).then(resolve);
        }, 0);
    });
}

recursivePromise(3);

console.log('Outside recursion');
		

Порядок вывода:

  1. Outside recursion
  2. Recursive 3
  3. Recursive 2
  4. Recursive 1

Объяснение:

  • Вызов recursivePromise(3) запускает цепочку промисов и таймеров.
  • Синхронный код (console.log('Outside recursion')) выполняется первым.
  • Event loop последовательно обрабатывает каждый вызов рекурсии, начиная с самого глубокого уровня.

Параллельные сетевые запросы и обработка результатов

			function fetchData(url) {
    return new Promise((resolve) => {
        setTimeout(() => {
            console.log(`Fetched data from ${url}`);
            resolve(url);
        }, Math.random() * 1000);
    });
}

Promise.all([
    fetchData('https://api.example.com/data1'),
    fetchData('https://api.example.com/data2'),
    fetchData('https://api.example.com/data3')
]).then(results => {
    console.log('All data fetched:', results);
});

console.log('Starting fetches');
		

Порядок вывода:

  1. Starting fetches
  2. Fetched data from ... (в случайном порядке)
  3. All data fetched: [...]

Объяснение:

  • Все сетевые запросы начинаются параллельно, но завершаются в разное время из-за случайной задержки.
  • Event loop обрабатывает завершение каждого запроса по мере их выполнения.
  • После завершения всех запросов выполняется обработчик Promise.all.

Эти примеры показывают, как event loop управляет асинхронными операциями и как важно понимать порядок выполнения кода для правильного написания асинхронных приложений.

Лайфхаки для понимания Event Loop

Используйте визуализацию

Используйте инструменты вроде Chrome DevTools или онлайн-симуляторов Event Loop, чтобы видеть порядок выполнения задач.

Создайте шаблоны

Создайте шаблонные ситуации и попробуйте изменять их, чтобы лучше понять влияние различных элементов.

Регулярно практикуйтесь

Регулярно решайте задачи и пишите код, связанный с асинхронностью, чтобы закрепить знания.

Изучите документацию

Изучите официальную документацию по JavaScript и спецификации ECMAScript для более глубокого понимания.

Сегодня мы получили базовое представление о том, как работает Event Loop, его особенности и возможные подводные камни. Практикуйтесь и экспериментируйте, чтобы лучше понять этот важный механизм.

Следите за новыми постами
Следите за новыми постами по любимым темам
314 открытий2К показов