Макротзадачи, микрозадачи, контексты выполнения, очереди событий и rAF

Угадайте результат кода ниже:

Я дам вам несколько вариантов.

  1. start, foo, bar, end, rep, foo, baz, liz
  2. start, foo, foo, bar, baz, liz, rep, end
  3. start, end, foo, bar, foo, baz, liz, rep
  4. start, foo, bar, end, foo, baz, liz, rep

Если вы не знаете ответа или ваш ответ от 1 до 4, вы попали в нужное сообщение. Это была довольно сложная викторина. К сожалению, ни один из вышеперечисленных вариантов не может считаться ответом на викторину.

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

Но я уверен, что вы поймете рабочий процесс цикла событий в JavaScript после прочтения моего сообщения!

  • Стек контекста выполнения.
  • Как JavaScript работает с задачами - очередь задач.
  • SetTimeout - Макрозадача.
  • RequestAnimationFrame - шаги рендеринга JavaScript.
  • Микрозадача - Обещание, queueMicrotask.

Связанный контент

Возможно, вас заинтересуют другие мои посты, связанные с некоторыми концепциями, которые будут упомянуты в этом посте.

Я рекомендую вам прочитать мои сообщения или документацию MDN, чтобы лучше понять, как работает событие JavaScript.

Отказ от ответственности

Внимание, народ! В этом посте говорится только о JavaScript в браузере, а не о серверном JavaScript, Node.js!

Я мало что знаю о Node, поэтому он может отличаться от JavaScript в браузере. Я мог бы использовать несколько примеров для лучшего понимания цикла событий, но этот пример может не совпадать с тем, как работает JavaScript.

Некоторые истории могут сильно отличаться от реальной модели JavaScript.

Стек контекста выполнения

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

Выполнение функций JavaScript отличается от их объявления. Они не выполняются, если они не являются IIFE. Но как только они вызываются, для области функции создается новая область, которая называется контекстом выполнения функции.

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

Стек, который управляет контекстами выполнения, называется стеком вызовов контекста выполнения (или стеком контекста выполнения).

Тем не менее, размер стека не бесконечен, поэтому, если в стек помещено слишком много контекстов, он переполнится, и вы увидите следующее сообщение:

В этом примере a вызывает себя рекурсивно и непрерывно, пока кто-нибудь (движок JavaScript) не остановит его.

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

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

Как JavaScript работает с задачами - очередь задач

В отличие от стека, который работает как LIFO, каждая задача или задание JavaScript, или что бы вы ни вызывали, складывается в очередь, которая работает как FIFO. Эта очередь называется очередью задач.

На самом деле, спецификация JavaScript говорит, что очереди задач - это наборы, а не очереди.

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

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

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

console.log(1);
while (true);
console.log(2);

2 никогда не будет напечатан, потому что он заблокирован while (true). Что это в основном означает?

Сначала будет напечатано 1, затем JavaScript выполнит второй оператор while. Он помещает задачу в очередь, и задача выполняется.

Но условие while равно true, поэтому JavaScript запускает while еще раз, помещает другую задачу в очередь, и задача выполняется.

Но состояние while равно true, поэтому JavaScript запускается while еще раз, и он подталкивает другую задачу к ...

На рисунке выше показано, как console.log(1) был помещен в очередь задач и удален. Как только JavaScript находит оператор для выполнения, он помещает задачу в очередь задач и запускает ее.

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

Это происходит постоянно, потому что состояние цикла всегда true.

Итак, console.log(2) даже не может попасть в очередь задач. Если JavaScript не может выполнять другие коды так долго, он показывает вам предупреждающее сообщение о выходе из бесконечного цикла.

Терминология для очереди задач может отличаться в документации. Некоторые используют очередь сообщений.

SetTimeout - Макрозадача

Поговорим о setTimeout. Попробуйте угадать результат этого кода.

console.log(1);
setTimeout(() => console.log(2), 100);
console.log(3);
setTimeout(() => console.log(4), 10);

Итак, я полагаю, вы уже знаете, что setTimeout запускает коды позже, когда выполняются все другие синхронные коды.

Сначала вы должны понять, как JavaScript запускает таймеры. В самом JavaScript не все методы API, которые вы использовали.

Например, в браузере Chrome вы могли использовать document с JavaScript, потому что движок Chrome помещает компьютерный язык, JavaScript, вместе с другими модулями API, подобными сторонним библиотекам.

А document является частью модулей в модулеDOM, который является одним из API, называемых веб-API.

setTimeout не является чистым методом ECMAScript. Он принадлежит window API, который также является одним из API, которого нет в ECMAScript. Но если вы запустите setTimeout в своем коде, он станет исполняемым.

Это потому, что основной движок под капотом смешивает JavaScript со всеми другими API-интерфейсами, необходимыми для работы в браузере.

Однако после вызова setTimeout JavaScript отправляет запрос (это не сетевой запрос!) API с setTimeout и переходит к следующей строке кода.

OK. Для лучшего понимания все расскажу на примере истории.

Вот довольно модный ресторан в отеле. На данный момент довольно много пустых столов.

А в ресторане работают двое рабочих. Менеджер (человек слева), который устраивает очереди и позволяет клиентам входить или управляет резервированием клиентов.

И официант, человек справа, который принимает заказ и убирает со стола.

Каждый покупатель должен ждать в очереди, пока менеджер не скажет: «Хорошо, теперь у нас есть для вас столик, заходите».

В линии есть два покупателя, звезда и многоугольник. Менеджер говорит, что звезда может войти внутрь, но многоугольнику нужно подождать еще немного. И теперь официант в ресторане подает еду клиентам.

В этом примере диспетчер - это очередь задач в JavaScript. Очередь задач хранит задачи в ней, как менеджер ресторана держит клиентов в очереди.

А официант - это движок JavaScript. Это дает поворот / контроль за действием для контекста выполнения, например, официант подает еду клиентам.

Допустим, в очереди в очереди в настоящий момент находится покупатель в форме звезды console.log(1). И он поставлен в очередь. (Покупатель ждет в очереди).

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

Следующим клиентом, многоугольником, является console.log(3), и он может войти внутрь, как только первый клиент закончит есть свою еду, что означает, что работа закончилась, поэтому 1 было напечатано в консоли браузера.

Таким же образом будет напечатан 3.

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

Нравится.

Розовые клиенты вообще не бронировали. Но желтые клиенты забронировали. Тогда что бы вы сделали, если бы вы были менеджером заведения?

Конечно, вы должны позволять входить только клиентам, которые забронировали номер. Таким образом, менеджер извиняется и заставляет ждать в очереди «клиентов, которые не бронируют».

Это кажется вполне рациональной системой. Но на самом деле эти розовые покупатели вообще не говорят по-английски.

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

Итак, правильная цифра будет следующей.

Менеджер сообщил агентству, в какой очереди должны были ждать розовые покупатели.

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

Розовые покупатели - 66 лет. Когда JavaScript читает setTimeout, он откладывает его и помещает в другую очередь, как менеджер заставил розовых клиентов ждать в другой очереди тех, кто не сделал бронь.

И JavaScript выполняет другие коды, такие как console.log, прежде чем он вызовет setTimeout.

Я сказал вам, что setTimeout не включен в ECMAScript. Он принадлежит window. Если JavaScript видит setTimeout, он отправляет запрос в нужный API для обработки вызова setTimeout, в этом случае.

Затем модуль API сохраняет его и помещает в другую очередь для незарегистрированных клиентов за x миллисекунды, что является вторым параметром setTimeout.

setTimeout(() => console.log(2), 100);
setTimeout(() => console.log(4), 10);

Первый setTimeout отправляется в API раньше второго. Но второй добавляется в очередь перед первым из-за времени задержки.

Модуль API для setTimeout подталкивает второй за 10 мсек. Тогда очередь для setTimeouts будет выглядеть так.

------------------------
log(4)   |   log(2)  |
------------------------

Эта новая очередь задач называется очередью макрозадач. Очередь макрозадач работает так же, как очередь задач. Но разница между ними в том, что очередь задач предназначена для синхронных операторов, а очередь макрозадач предназначена для асинхронных операторов.

Теперь вы можете угадать правильный ответ в этой викторине.

console.log(1);
setTimeout(() => console.log(2), 100);
console.log(3);
setTimeout(() => console.log(4), 10);
// 1 -> 3 -> 4 -> 2

RequestAnimationFrame - шаги отрисовки JavaScript

Итак, вы подошли к части того, как JavaScript работает с setTimeout как асинхронной функцией.

Однако многие из вас могут знать, что сейчас по многим причинам лучше использовать rAF, requestAnimationFrame, чем setTimeout.

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

rAF делает то же самое, что и setTimeout. При вызове он не выполняется. Но разве вам не интересно, что запущено раньше, setTimeout или rAF?

console.log(1);
setTimeout(() => console.log(2), 0);
requestAnimationFrame(() => console.log(3));

Давай снова вернемся в ресторан. Теперь у нас есть еще два клиента, которые тоже не бронировали. Итак, менеджер отправляет их в могущественное агентство, которое о них заботится.

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

Класс членства синих клиентов - Diamond, что выше, чем Platinum, что соответствует классу членства розовых клиентов. Итак, агентство спрашивает менеджера, что им делать с этим разным членством, и тот говорит: «Не беспокойтесь об этом, я справлюсь».

Тем временем наш верный работник, официант, так усердно работал на своей работе.

У бедного официанта не бывает даже короткого перерыва, но он счастлив! Он подает еду клиентам и убирает на столах.

Подача еды - это выполнение заявления. Очистка таблиц - это повторный рендеринг браузера. Механизм JavaScript, официант в этом примере ресторана, берет задачу из очереди задач и выполняет ее.

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

И обычно JavaScript отображает страницу каждые 16,6 мс. Затем он отображает страницу примерно 60 раз в минуту со скоростью 16,6 мс. Вот тут-то и пригодится 60FPS.

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

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

rAFs выполняются перед обработкой страницы механизмом JavaScript, а setTimeouts выполняются после процесса визуализации, например, клиенты класса Diamond могли войти в ресторан до того, как официант уберет столы, и после того, как стол будет очищен, могут войти клиенты класса Platinum.

rAF также является макрозадачей, поскольку он использует ту же очередь задач, что и setTimeout. И rAF может гарантировать выполнение до того, как сценарий будет проанализирован и страница будет перерисована, что означает, что он будет выполнен раньше, чем setTimeout.

Микрозадача - Обещание, очередь

Мы еще не говорили о обещании, это тоже было в викторине в начале этого поста.

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

Менеджер и официант уже привыкли к этому загруженному делу. У них так много разных клиентов: клиенты, которые бронировали (обычные задачи), клиенты, которые не бронировали, но на самом деле были участниками премиум-класса (макрозадачи).

Некоторые из них относились к классу Platinum (setTimeout), а некоторые - к классу Diamond (requestAnimationFrame). Но как бы то ни было, они уже привыкли к этому.

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

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

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

Вы поняли намек? Да, верно. Зеленый клиент - Promise. Как только JavaScript читает обещания, он помещает их в другую очередь, которая не является очередью задач или очередью макрозадач.

Это называется очередью микрозадач. Обратите внимание, что написание макрозадачи и микрозадачи очень похоже, но сильно отличается.

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

Но, конечно, микрозадачи имеют более высокий приоритет, чем макрозадачи.

Итак, что именно это означает?

console.log(1);
Promise.resolve().then(() => console.log(2));
setTimeout(() => console.log(3), 100);
console.log(4);
// 1 -> 4 -> 2 -> 3

console.log(1) и console.log(4) будут напечатаны первыми, поскольку это просто задачи. JavaScript уже прочитал Promise код в строке 2, но не выполняет его сразу.

Вместо этого он помещает его в очередь микрозадач, которая ожидает, когда все задачи будут исключены из очереди задач. И когда JavaScript читает setTimeout, он отправляет его в веб-API.

Модуль веб-API для setTimeout отправляет его в очередь макрозадач через 100 мсек. При выполнении макрозадач в очереди макрозадач выполняются все задачи и выполняются все микрозадачи.

Но в отличие от requestAnimationFrame, setTimeout не заботится о шагах рендеринга. Они просто отправляются в очередь макрозадач через несколько миллисекунд и ждут своей очереди.

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

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

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

Резюме

Наконец-то. Вы так хорошо сделали, что прочитали все сложные концепции. Я кратко резюмирую то, о чем говорил в этом посте.

Очередь задач - это очередь, в которой хранятся задачи JavaScript. Это может быть изменение цвета элемента DOM или увеличение количества значений. Задачи JavaScript имеют наивысший приоритет для выполнения.

Очередь макрозадач - это очередь для setTimeout и requestAnimationFrame. На самом деле, есть больше задач, которые считаются очередью макрозадач, пожалуйста, проверьте ссылку, которую я прикрепил в разделе ресурсы.

setTimeout и requestAnimationFrame - асинхронные функции. И они являются частью модуля веб-API window. Итак, JavaScript отправляет их в модуль API, а модуль в нужный момент помещает их в очередь макрозадач.

setTimeout, однако, может беспокоить интервалы между кадрами рендеринга, которые составляют 16,6 мс, поскольку они выполняются после рендеринга. Если функция обратного вызова в setTimeout выполняется бесконечно, выполнение следующего тика для рендеринга также будет отложено.

Здесь на помощь приходит requestAnimationFrame. rAF выполняется перед рендерингом.

Очередь микрозадач - это очередь для Promise - конечно, это не просто Promise.

Следует помнить, что микрозадачи выполняются только после того, как очередь задач полностью пуста. Если очередь задач не пуста, ни одна из микрозадач не выполняется. И микрозадачи выполняются до выполнения макрозадач.

Вернуться к викторине

Теперь я считаю, что вы можете угадать правильный ответ на эту викторину!

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

Safari печатает слова в другом порядке, потому что Safari запускается rAF после визуализации.

Заключение

Большое спасибо за то, что прочитали мой пост! Мне потребовалось так много времени, чтобы понять всю концепцию цикла событий JavaScript. Я читал одни и те же статьи снова и снова и несколько раз смотрел видео.

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

Больше в этой серии

Ресурсы