Event Loop in detail

META

Activist
SUPREME
MEMBER
Joined
Mar 1, 2026
Messages
118
Reaction score
378
Deposit
0$
---

В этой статье мы обсудим, почему был создан цикл событий (Event Loop), как с ним работать и почему о нем часто спрашивают на собеседованиях.

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

Почему слово «асинхронный» в кавычках? Потому что JavaScript на самом деле выполняет всё синхронно — в самом JavaScript нет истинного параллелизма. Давайте разберёмся (попробуем разобраться).


---

Синхронный код

Синхронный код довольно прост: интерпретатор проходит по каждой инструкции по очереди, выполняет её, и всё работает как положено.

пусть acc = 1;
console.log(acc); // 1

acc++;
console.log(acc); // 2

let anotherAcc = acc;
console.log(anotherAcc, acc); // 3 3


---

Асинхронный код

В случае с асинхронным кодом все немного усложняется. Рассмотрим следующий пример:

пусть acc = 1;
console.log("Вызов 1:", acc);

acc++;
console.log("Вызов 2:", acc);

setTimeout(() => {
acc++;
console.log("Вызов 3:", acc);
}, 0);

let anotherAcc = acc;
console.log("Вызов 4:", anotherAcc, acc);

/* Выход

Звонок 1: 1
Звонок 2: 2
Звонок 4: 2 2
Звонок 3: 3

*/

Как мы видим, вызов console.log внутри setTimeout выполняется позже. Почему это происходит?

Здесь вступает в игру цикл событий. Функция setTimeout является асинхронной операцией (таймер обрабатывается браузером, а не самим JavaScript).


---

Просматривая код, как интерпретатор.

Давайте разберем это шаг за шагом.

Мы объявляем переменные и выполняем операции. Все синхронные операции выполняются немедленно, как только интерпретатор их встречает.

Однако с таймером все иначе. Задержка по истечении времени ожидания обрабатывается браузером, поэтому операция фактически «исчезает» из основного потока выполнения. Функция обратного вызова помещается в цикл событий, где она ожидает, пока браузер не сообщит о завершении таймера.

Теперь о неочевидном моменте: даже если время ожидания истекло, но текущая функция (стек вызовов) все еще выполняет синхронный код, цикл событий будет ждать. Только после того, как стек вызовов опустеет, цикл событий вернет вызов на выполнение.


---

Углубляясь в тему

Как уже упоминалось ранее, JavaScript выполняет только одну операцию за раз. Все асинхронные операции делегируются циклу событий.

Возможно, вы слышали о макрозадачах и микрозадачах.

Цикл событий — это механизм, обеспечивающий асинхронное поведение. Он управляет задачами, которые не являются частью основного синхронного потока выполнения. После завершения синхронного кода начинают выполняться задачи из цикла событий.

Однако цикл событий имеет свои собственные правила. Он делит задачи на категории:

Микрозадачи

Макротаски

Задачи рендеринга


Макротаски

К ним относятся:

setTimeout

XMLHttpRequest

другие API браузера


Микрозадачи

К ним в основном относятся:

Обещание.

IntersectionObserver


Задачи рендеринга

Это связано с обновлением и отображением страницы.


---

Упрощенная реализация цикла событий

Если бы мы реализовали собственный цикл событий, он мог бы выглядеть так:

интерфейс Task {
выполнить: () => {};
}

интерфейс MacroTasks {
парсер: Task[];
ресурсы: Задача[];
domManipulation: Task[];
события: Задача[];
Обратные вызовы: Task[];
}

интерфейс EventLoop {
макрозадачи: MacroTasks;
микрозадачи: Задача[];
nextMacrotask: () => Task | null;
needRender: () => boolean;
render: () => void;
}

let isCallStackEmpty = false;

const eventLoop: EventLoop = {
макрозадачи: {
парсер: [],
ресурсы: [],
domManipulation: [],
события: [],
обратные вызовы: [],
},

микрозадачи: [],

nextMacrotask() {
for (const section in this.macrotasks) {
const queue = this.macrotasks[section as keyof typeof this.macrotasks];
if (queue.length) {
return queue.shift() ?? null;
}
}
вернуть null;
},

needRender: () => false,
оказывать() {}
};

пока (true) {
if (!isCallStackEmpty) continue;

for (const task of eventLoop.microtasks) {
task.execute();
}
eventLoop.microtasks = [];

const task = eventLoop.nextMacrotask();
если (задача) {
task.execute();
}

if (eventLoop.needRender()) {
eventLoop.render();
}
}


---

Как работает цикл событий

Внутри цикла:

1. Сначала проверяется, пуст ли стек вызовов.


2. Затем выполняется выполнение всех микрозадач.


3. Очищает очередь микрозадач.


4. Выполняет одну макрозадачу.


5. Проверяет, требуется ли рендеринг.


6. При необходимости выполняет рендеринг.


7. Цикл повторяется.




---

Эксперименты ✨

Давайте проверим это поведение:

console.log("Шаг 1: В глобальной области видимости");

setTimeout(() => console.log("Шаг 2: В setTimeout"));

new Promise((resolve) => {
console.log('Шаг 3: В конструкторе промиса');
}).then(() => console.log('Шаг 4: В then'));

setTimeout(() => console.log("Шаг 5: В другом вызове setTimeout"));

Выход:

Шаг 1
Шаг 3
Шаг 4
Шаг 2
Шаг 5

Порядок исполнения:

Синхронный: Шаг 1, Шаг 3

Микрозадачи: Шаг 4

Макротаски: Шаг 2, Шаг 5


Очереди работают по принципу FIFO (первым пришел — первым вышел).


---

Два затем

new Promise((resolve) => {
console.log('Шаг 3');
}).then(() => console.log('Шаг 4'))
.then(() => console.log('Шаг 5'));

Ничего удивительного — просто добавлена еще одна микрозадача.


---

Два обещания

new Promise(resolve => {
console.log('Шаг 3');
решать();
}).then(() => console.log('Шаг 4'))
.then(() => console.log('Шаг 5'));

new Promise(resolve => {
console.log('Шаг 7');
решать();
}).then(() => console.log('Шаг 8'))
.then(() => console.log('Шаг 9'));

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

Шаг 3
Шаг 7
Шаг 4
Шаг 8
Шаг 5
Шаг 9

Почему? Потому что каждый из них затем планирует новую микрозадачу после завершения предыдущей.


---

Микрозадача внутри макрозадачи

setTimeout(() => console.log('Шаг 1'));

new Promise(resolve => {
console.log('Шаг 2');
решать();
}).then(() => {
console.log('Шаг 3');
setTimeout(() => console.log('Шаг 4'));
});

setTimeout(() => console.log('Шаг 5'));

Шаг 4 выполняется последним, поскольку он был добавлен в очередь макрозадач позже.


---

Макротакция с микрозадачей внутри

setTimeout(() => {
new Promise(resolve => {
console.log('Шаг 2');
решать();
}).then(() => console.log('Шаг 3'));
});

Микрозадачи запускаются сразу после завершения текущей макрозадачи.


---

Заключение 🌚

Если вам понравилась эта статья, загляните в мой блог, где вы найдете больше материалов по веб-разработке.

Если у вас возникнут вопросы, не стесняйтесь задавать их в комментариях. Хорошего дня! 💁🏻‍♂️
 
Top Bottom