DEV Community

Cover image for ✨♻️ Визуализируя JavaScript. Часть 2/7: цикл событий [перевод]
Mari Kalyuzhna
Mari Kalyuzhna

Posted on

✨♻️ Визуализируя JavaScript. Часть 2/7: цикл событий [перевод]

Данный текст является переводом оригинальной статьи ✨♻️ JavaScript Visualized: Event Loop авторства ****Lydia Hallie. В процессе адаптации на русский язык я попыталась сохранить стиль автора.

Статья является частью серии из 7-ми текстов, в которой Лидия доступно, с живыми иллюстрациями и юмором, раскрывает одни из самых базовых и, вместе с тем, сложных для понимания концепций и принципов JavaScript.

Введение

«О боже, цикл событий» — так наверняка думал хотя бы раз в своей жизни любой frontend–разработчик. Это одна из тех вещей, с которыми так или иначе приходится сталкиваться каждому JavaScript разработчику чуть ли не каждый день, но поначалу концепция Event Loop кажется запутанной. Я визуал и умею наглядно представлять информацию, поэтому решила попытаться помочь вам, объяснив это визуально с помощью гифок с низким разрешением, потому что на дворе 2019 год [на момент написания оригинального поста], а гифки почему-то все еще пиксельные и размытые.

Но прежде всего, что такое цикл событий и почему вас это должно волновать?

JavaScript является однопоточным : одновременно может выполняться только одна задача. Обычно в этом нет ничего страшного, но теперь представьте, что вы выполняете задачу, которая занимает 30 секунд... М-да... Во время этой задачи мы ждем 30 секунд, прежде чем что-то еще может произойти (JavaScript по умолчанию запускается в основном потоке браузера, поэтому весь интерфейс завис) 😬 На дворе 2019 год, никому не нужен медленный и не отвечающий сайт.

К счастью, браузер предоставляет нам некоторые функции, которых нет в самом движке JavaScript: Web API. Сюда входят DOM API, setTimeout, HTTP-запросы и т. д. Это может помочь нам создать асинхронное, поведение, которое не будет блокировать основной поток 🚀

Стек вызовов

Когда мы вызываем функцию, она добавляется в нечто, называемое стеком вызовов (call stack). Стек вызовов является частью движка JS и не зависит от браузера. Это очередь, что означает “первый пришёл, последний ушёл” (представьте себе стопку блинов). Когда функция возвращает значение, оно извлекается из стека 👋

Стек вызовов — это структура данных, которая, говоря упрощённо, записывает сведения о месте в программе, где мы находимся. Если мы переходим в функцию, мы помещаем запись о ней в верхнюю часть стека. Когда мы из функции возвращаемся, мы вытаскиваем из стека самый верхний элемент и оказываемся там, откуда вызывали эту функцию. Это — всё, что умеет стек.

1 || Функции помещаются в стек вызовов, когда мы их вызываем, и извлекаются, когда возвращают какой-то результат

1 || Функции помещаются в стек вызовов, когда мы их вызываем, и извлекаются, когда возвращают какой-то результат

Очередь вызовов и setTimeout

Функция respond возвращает setTimeout функцию. setTimeout предоставляется Web API: он позволяет нам откладывать задачи на заданное время, не блокируя основной поток. Функция обратного вызова (коллбэк), которую мы передали функции setTimeout, стрелочная функция () => { return 'Hey'}, добавляется в Web API. Тем временем setTimeout функция и функция respond извлекаются из стека, они обе вернули свои значения!

2 || setTimeout предоставляется нам со стороны браузера, Web API позаботится о коллбэке, который мы передали

2 || setTimeout предоставляется нам со стороны браузера, Web API позаботится о коллбэке, который мы передали

В Web API таймер работает столько, сколько мы указали во втором аргументе, — 1000 мс (по умолчанию время в JS указывается в мс). Коллбэк не добавляется сразу в стек вызовов, вместо этого он откладывается в нечто, называемое очередью.

3 || Когда время таймера закончится (1000 мс, в данном случае), коллбэк будет помещён в очередь вызовов

3 || Когда время таймера закончится (1000 мс, в данном случае), коллбэк будет помещён в очередь вызовов

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

Очереди работают по принципу “первый пришёл, первый вышел” (представьте себе очередь в магазине).

Наконец, часть, которую мы все ждали… Пришло время циклу событий выполнить свою единственную задачу: соединить очередь со стеком вызовов! Если стек вызовов пуст, т. е. все ранее вызванные функции вернули свои значения и были извлечены из стека, первый элемент из очереди добавляется в стек вызовов. В этом случае никакие другие функции не вызывались, а это означает, что стек вызовов был пуст к тому моменту, когда функция обратного вызова стала первым элементом в очереди.

4 || Цикл событий смотрит на очередь коллбэков и на стек вызовов. Есть стек вызовов пуст, в него перемещается первый элемент из очереди коллбэков

4 || Цикл событий смотрит на очередь коллбэков и на стек вызовов. Есть стек вызовов пуст, в него перемещается первый элемент из очереди коллбэков

Наконец, коллбэк добавляется в стек вызовов, вызывается, возвращает значение и извлекается из стека. Ура! 🎉

5 || Коллбэк добавляется в стек вызовов и выполняется. После того, как функция возвращает значение, они извлекается из стека вызовов

5 || Коллбэк добавляется в стек вызовов и выполняется. После того, как функция возвращает значение, они извлекается из стека вызовов

Читать статью весело, но вам станет комфортно со стеком вызовов только тогда, когда будете работать с ним снова и снова. Попытайтесь выяснить, что выведется в консоль, если мы запустим следующей код:

const foo = () => console.log("First");
const bar = () => setTimeout(() => console.log("Second"), 500);
const baz = () => console.log("Third");

bar();
foo();
baz();
Enter fullscreen mode Exit fullscreen mode

Разобрались? Давайте быстро взглянем, что происходит, “за кулисами” когда мы запускаем этот код в браузере:

Image description

  1. Мы вызываем bar.bar возвращает таймер setTimeout.
  2. Коллбэк, который мы передали в setTimeout, добавляется в Web API, функции setTimeout и bar извлекаются из стека вызовов.
  3. Таймер запускается, тем временем foo вызывается и логирует First. foo возвращает *undefined*.
  4. Вызывается baz и попадает в стек вызовов, тем временем, коллбэк таймера добавляется в очередь.
  5. baz логирует Third. Цикл событий видит, что стек вызовов пустеет после того, как из baz возвращается *undefined*. Теперь коллбэк таймера добавляется в стек вызовов.
  6. Наконец, логируется Second. Коллбэк отработал, вернул *undefined* и вышел из стека вызовов.

Заключение

Надеюсь, что благодаря этому посту вы почувствуете себя более комфортно с циклом событий! Не волнуйтесь, если это все еще кажется запутанным, самое главное — понять, откуда могут возникнуть определенные ошибки/поведение, чтобы эффективно находить в Google нужные термины и, в конечном итоге, оказаться на правильной странице Stack Overflow 💪🏼 Не стесняйтесь обращаться к автору, если у вас есть вопросы!



Lydia Hallie в соцсетях: Twitter || Instagram || GitHub || LinkedIn || Website

P.S. Автор использовала Keynote для создания анимаций и записей экрана.

P.P.S. В других статьях будут рассмотрены такие штуки, как Поднятие (hoisting), область действия (scope chain), прототипное наследование (prototypal inheritance), генераторы и итераторы (generators ans iterators), промисы (promises) и async/await.

Top comments (0)