1 of 51

«Потоки» в JavaScript

Обработка событий, очереди, вот это всё…

2 of 51

Порядок обработки событий

События могут возникать не только по очереди, но и «пачкой» по много сразу. Возможно и такое, что во время обработки одного события возникают другие, например, �пока выполнялся код для onclick – посетитель нажал кнопку на клавиатуре (событие keydown).

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

3 of 51

Главный поток

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

Он выполняет команды последовательно, может делать только одно дело одновременно и блокируется при выводе модальных окон, таких как alert.

4 of 51

Очередь событий

Произошло одновременно несколько событий или во время работы одного случилось другое – то, как главному потоку обработать это?

Если главный поток прямо сейчас занят, то он не может срочно выйти из середины одной функции и прыгнуть в другую. А потом третью. Отладка при этом могла бы превратиться в кошмар, потому что пришлось бы разбираться с совместным состоянием нескольких функций сразу.

Поэтому, когда происходит событие, оно попадает в очередь.

5 of 51

Event loop

Внутри браузера непрерывно работает «главный внутренний цикл», который следит за состоянием очереди и обрабатывает события, запускает соответствующие обработчики и т.п.

Идея событийного цикла очень проста. Есть бесконечный цикл, в котором движок JavaScript ожидает задачи, исполняет их и снова ожидает появления новых.

6 of 51

Множество событий

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

Например, при клике на элементе генерируется несколько событий:

Сначала mousedown – нажата кнопка мыши.

Затем mouseup – кнопка мыши отпущена и, так как это было над одним элементом, то дополнительно генерируется click (два события сразу).

7 of 51

Множество событий в действии

8 of 51

Вложенные (синхронные) события

Обычно возникающие события «становятся в очередь».

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

Рассмотрим в качестве примера событие onfocus.

9 of 51

событие onfocus

Когда посетитель фокусируется на элементе, возникает событие onfocus.

Обычно оно происходит, когда посетитель кликает на поле ввода, например:

10 of 51

событие onfocus

Но ту же фокусировку можно вызвать и явно,

вызовом метода elem.focus():

11 of 51

12 of 51

Вложенные (синхронные) события

13 of 51

Event loop

Вернёмся к событийному циклу.

Движок JavaScript большую часть времени ничего не делает и работает, только если требуется исполнить скрипт/обработчик или обработать событие.

Задачи поступают на выполнение – движок выполняет их – затем ожидает новые задачи

Может так случиться, что задача поступает, когда движок занят чем-то другим, тогда она ставится в очередь.

Очередь, которую формируют такие задачи, называют �«очередью макрозадач» (macrotask queue).

14 of 51

Event loop

Отметим две детали:

  • Рендеринг (отрисовка страницы) никогда не происходит во время выполнения задачи движком. Не имеет значения, сколь долго выполняется задача. Изменения в DOM отрисовываются только после того, как задача выполнена.
  • Если задача выполняется очень долго, то браузер не может выполнять другие задачи, обрабатывать пользовательские события, поэтому спустя некоторое время браузер предлагает «убить» долго выполняющуюся задачу. Такое возможно, когда в скрипте много сложных вычислений или ошибка, ведущая к бесконечному циклу.

15 of 51

Пример сообщений «подвисшей» вкладки

16 of 51

Обработка «тяжёлой» задачи

Допустим, у нас есть задача, требующая значительных ресурсов процессора.

Например, подсветка синтаксиса (используется для выделения цветом участков кода на странице) – довольно процессороёмкая задача. Для подсветки кода надо выполнить синтаксический анализ, создать много элементов для цветового выделения, добавить их в документ – для большого текста это требует значительных ресурсов.

17 of 51

Обработка «тяжёлой» задачи

Пока движок занят подсветкой синтаксиса, он не может делать ничего, связанного с DOM, не может обрабатывать пользовательские события и т.д. �Возможно даже «подвисание» браузера, что совершенно неприемлемо.

Мы можем избежать этого, разбив задачу на части. Сделать подсветку для первых 100 строк, затем запланировать setTimeout �(с нулевой задержкой) для разметки следующих 100 строк и т.д.

18 of 51

Обработка «тяжёлой» задачи

Чтобы продемонстрировать такой подход, давайте будем использовать для простоты функцию, �которая считает от 1 до 1000000000.

19 of 51

А теперь давайте разобьём задачу на части, воспользовавшись вложенным setTimeout

20 of 51

Обработка «тяжёлой» задачи

Теперь интерфейс браузера полностью работоспособен во время выполнения «счёта».

Один вызов count делает часть работы (*), а затем, если необходимо, планирует свой очередной запуск (**):

Первое выполнение производит счёт: i=1…1000000.

Второе выполнение производит счёт: i=1000001…2000000 и так далее.

Теперь если новая сторонняя задача (например, событие onclick) появляется, пока движок занят выполнением 1-й части, то она становится в очередь, и затем выполняется, когда 1-я часть завершена, перед следующей частью. Периодические возвраты в событийный цикл между запусками count дают движку достаточно «воздуха», чтобы сделать что-то ещё, отреагировать на действия пользователя.

21 of 51

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

Помимо макрозадач, т.е. тех что мы рассмотрели ранее, существуют микрозадачи.

Микрозадачи приходят только из кода. Обычно они создаются промисами: выполнение обработчика cтановится микрозадачей.

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

22 of 51

Promise

Но прежде чем разбираться в отличиях микро и макро задач, давайте узнаем, кто же такие эти ваши промисы?!

23 of 51

Promise (пример из реальной жизни)

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

Чтобы получить передышку, вы обещаете разослать им сингл, когда он будет выпущен. Вы даёте фанатам список, в который они могут записаться. Они могут оставить там свой e-mail, чтобы получить песню, как только она выйдет. И даже больше: если что-то пойдёт не так, например, в студии будет пожар и песню выпустить не выйдет, они также получат уведомление об этом.

24 of 51

Promise

  • Есть «создающий» код, который делает что-то, что занимает время. Например, загружает свою песню в сеть. �В нашей аналогии это – «певец».

  • Есть «потребляющий» код, который хочет получить результат «создающего» кода, когда он будет готов. �Он может быть необходим более чем одной функции. �В нашей аналогии это – «фанаты».

25 of 51

Promise

это специальный объект в JavaScript, который связывает «создающий» и «потребляющий» коды вместе. �В терминах нашей аналогии – это «список для подписки». �

«Создающий» код может выполняться сколько потребуется, �чтобы получить результат, ��а промис делает результат доступным для кода, �который подписан на него, когда результат готов.

26 of 51

Promise

Аналогия не совсем точна, потому что объект Promise в JavaScript гораздо сложнее простого списка подписок: он обладает дополнительными возможностями и ограничениями. �Но для начала и такая аналогия хороша.

27 of 51

Promise

Функция, переданная в конструкцию new Promise, называется исполнитель (executor). �Когда Promise создаётся, она запускается автоматически. �Она должна содержать «создающий» код, который когда-нибудь создаст результат. �В терминах нашей аналогии: исполнитель – это «певец».

Её аргументы resolve и reject – это колбэки, которые предоставляет сам JavaScript. �Наш код – только внутри исполнителя.

28 of 51

Promise callbacks

Когда исполнитель получает результат, сейчас или когда то там потом – не важно, он должен вызвать один из этих колбэков:

resolve(value) — если работа завершилась успешно, �с результатом value.

reject(error) — если произошла ошибка, �error – объект ошибки.

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

29 of 51

Promise state

У объекта promise, возвращаемого конструктором new Promise, есть внутренние свойства:

state («состояние») — вначале "pending" («ожидание»), потом меняется на "fulfilled" («выполнено успешно») при вызове resolve или на "rejected" («выполнено с ошибкой») при вызове reject.

result («результат») — вначале undefined, далее изменяется на value при вызове resolve(value) �или на error при вызове reject(error).

30 of 51

Promise

31 of 51

Fulfilled Promise

Ниже пример конструктора Promise и простого исполнителя с кодом, дающим результат с задержкой (через setTimeout):

32 of 51

Rejected Promise

А теперь пример, в котором исполнитель сообщит, что задача выполнена с ошибкой:

33 of 51

34 of 51

Потребление Promise’ов

Объект Promise служит связующим звеном между исполнителем («создающим» кодом или «певцом») и функциями-потребителями («фанатами»), которые получат либо результат, либо ошибку. Функции-потребители могут быть зарегистрированы (подписаны)� с помощью методов:

.then()

.catch()

.finally()

35 of 51

Потребление Promise’ов

Then, catch и finally работают по аналогии с блоком

try {

} catch {

} finally {

}

36 of 51

Promise .then()

  • Аргумент метода .then – функция, которая выполняется, когда промис переходит в состояние «выполнен успешно», и получает результат.

37 of 51

Promise .catch()

  • Аргумент метода .catch – функция, которая выполняется, когда промис переходит в состояние «завершён с ошибкой», и получает объект ошибки:

38 of 51

Promise .finally()

Обработчик, вызываемый из finally, не имеет аргументов. В finally мы не знаем, как был завершён промис. И это нормально, потому что обычно наша задача – выполнить «общие» завершающие процедуры.

Это очень удобно, потому что finally не предназначен для обработки результата промиса. Так что он просто пропускает его через себя дальше.

39 of 51

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

Вернёмся к нашим микрозадачам…

Микрозадачи приходят только из кода. Обычно они создаются промисами: выполнение обработчика .then/catch/finally становится микрозадачей. Микрозадачи также используются «под капотом» await, т.к. это форма обработки промиса.

Отметим ещё раз, что…

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

40 of 51

Event loop

41 of 51

Самопроверка

42 of 51

Микрозадачи

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

Это важно, так как гарантирует, что общее окружение остаётся одним и тем же между микрозадачами – не изменены координаты мыши, не получены новые данные по сети и т.п.

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

43 of 51

Самопроверка

44 of 51

45 of 51

Appendix: async / await syntax sugar

Существует специальный синтаксис для работы с промисами, который называется «async/await». Он удивительно прост для понимания и использования.

46 of 51

async

У слова async один простой смысл: эта функция всегда возвращает промис. Значения других типов оборачиваются в завершившийся успешно промис автоматически.

47 of 51

await

Ключевое слово await заставит интерпретатор JavaScript ждать до тех пор, пока промис справа от await не выполнится. После чего оно вернёт его результат, и выполнение кода продолжится.

В примере ниже промис успешно выполнится через 1 секунду:

48 of 51

await

По сути, это просто «синтаксический сахар» для получения результата промиса, более наглядный, чем promise.then

49 of 51

Обработка асинхронных ошибок

50 of 51

Обработка асинхронных ошибок

Пример кода для обработки ошибок, т.е. аналогия Promise.catch():

51 of 51

Итого

Ключевое слово async перед объявлением функции:

  • Обязывает её всегда возвращать промис.
  • Позволяет использовать await в теле этой функции.

Ключевое слово await перед промисом заставит JavaScript дождаться его выполнения, после чего:

  • Если промис завершается с ошибкой, будет сгенерировано исключение, как если бы на этом месте находилось throw.
  • Иначе вернётся результат промиса.