Что такое цикл событий, как он работает и почему о нем все всегда спрашивают на собеседованиях? Некоторое время я не мог четко ответить на этот вопрос, а уже потом, когда набрался опыта и сам начал нанимать людей, стало понятно, что это реально большой пробел у большинства JavaScript-разработчиков.
1Что такое Event Loop в Node.js
Event Loop — это цикл, с помощью которого Node.js может выполнять неблокирующие операции I/O — input/output.
И еще очень важно, что Event Loop — это «сердце» большого механизма известной библиотеки libuv. Все, что нужно знать о libuv, я описал в своей статье «Фундамент для JavaScript-разработчика: как ответить, что такое libuv на собеседовании по Node.js» .
Теперь мы рассмотрим, что внутри Event Loop:
Фазы — это то, о чем далеко не все знают или говорят, но знание фаз и есть фундамент для понимания последовательности выполнения кода, написанного на JavaScript.
2Расскажите о фазах
- Timers: фаза, в которой выполняются коллбэки, запланированы
setTimeout()
иsetInterval()
. - Pending callbacks: выполняет I/O-коллбэки, отложенные до следующей итерации цикла.
- Idle, prepare: использовать только внутри.
- Poll: получение новых событий I/O; выполнять коллбэки, связанные с I/O (почти все, за исключением выполняемых в фазе коллбэков
close callbacks
, запланированных таймерами иsetImmediate()
); при необходимости node может здесь блокироваться. - Check: здесь вызываются
setImmediate() callbacks
. - Сlose callbacks: закрывает коллбэки, такие как
socket/http/eventEmitter/.on(‘close', () =>)
.
С полной информацией о фазах можно ознакомиться здесь.
3Что такое микро- и макрозадачи
После вопросов «что такое Event Loop» и «что вы знаете о фазах» спрашивают, знаете ли вы, что такое микрозадачи и макрозадачи, в ответ на этот вопрос у меня тоже есть статья .
А сейчас мы разберем следующее:
- разницу между
setTimeout()
isetImmediate()
; - разницу между
process.nextTick()
иsetImmediate()
; - какие трудности можно решить с помощью
process.nextTick()
.
4В чем разница между setTimeout() и setImmediate()
setTimeout()
— коллбэк, который мы передаем в таймер, выполняется после определенного пройденного времени, переданного вторым аргументом setTimeout()
или при отсутствии указанного времени, по умолчанию через 4 мс.
setImmediate()
выполняется после текущей poll-фазы:
const fs = require('fs'); fs.readFile(__filename, () => { setTimeout(() => { console.log('Timeout'); }, 0); setImmediate(() => { console.log('Immediate'); }); });
Результат:
Immediate Timeout
Но что важно, зависимость от контекста, в котором находятся функции, влияет на то, чей коллбэк выполнится первым.
Когда мы используем оба таймера в логике, работающей с I/O, как в примере сверху, то setImmediate()
будет всегда первым при условии, что передаваемые в них коллбэки будут схожи по логике или идентичны.
setTimeout(() => { console.log('timeout'); }, 0); setImmediate(() => { console.log('immediate'); });
В противном случае таймеры будут выполняться непредсказуемо. Ниже результат двух запусков таймеров:
Immediate Timeout
Timeout Immediate
5В чем разница между process.nextTick() и setImmediate()
В этой ситуации всегда первым выполняется process.nextTick()
, он выполняется при следующем тике (tick) ядра вашего компьютера, а это 100-1000 тиков в секунду — и в этом его опасность.
Если вы написали рекурсивную функцию и там есть process.nextTick()
, то текущий цикл Event Loop так может и не завершиться, об этом есть предостережение в официальной документации.
6Какие трудности можно решить с помощью process.nextTick()
Есть такие ситуации, когда нам нужно выполнить нашу функцию с минимальной задержкой только после того, как код будет инициализирован, но еще ни одна I/O-операция еще не будет выполнена, и здесь process.nextTick()
поможет нам с легкостью:
const EventEmitter = require('events'); class MyEmitter extends EventEmitter { constructor() { super(); process.nextTick(() => { this.emit('event'); }); } } const myEmitter = new MyEmitter(); myEmitter.on('event', () => { console.log('an event occurred!'); });
Этот пример кода из официальной документации, и очень простой, но он понятно и кратко показывает нам, какую важную проблему мы можем решить:
const EventEmitter = require('events'); class MyEmitter extends EventEmitter { constructor() { super(); this.emit('event'); } } const myEmitter = new MyEmitter(); myEmitter.on('event', () => { console.log('an event occurred!'); });
Если исключить process.nextTick()
из нашего примера, то событие event
никогда не сработает, потому что вызов произошел еще в конструкторе класса MyEmitter
, а регистрация события — позже.
Вывод
Если говорить про опыт, то у меня были кандидаты из других стран, которые на вопрос «что такое Event Loop» отвечали так: «Зачем вы задаете мне такой элементарный вопрос, я человек с опытом 5+ лет, и клиент за такие знания не платит, он платит за фичи».
Конечно, все платят за функционал, только вот интересно, сколько будет потрачено времени и каким будет качество кода, если ты не знаешь, как все работает и какая сила в наших руках.
Знание таких вещей как фазы и работа с ними помогает нам понимать более проблемные места в коде, писать код осмысленно и решать более нетривиальные задачи без «костылей», что влияет на надежность и уверенность в работе нашей системы.
Благодарю вас за внимание и продуктивного кодинга 😉
Этот материал – не редакционный, это – личное мнение его автора. Редакция может не разделять это мнение.
Сообщить об опечатке
Текст, который будет отправлен нашим редакторам: