что такое макро и микро задачи в js
Событийный цикл: микрозадачи и макрозадачи
Поток выполнения в браузере, равно как и в Node.js, основан на событийном цикле.
Понимание работы событийного цикла важно для оптимизаций, иногда для правильной архитектуры.
В этой главе мы сначала разберём теорию, а затем рассмотрим её практическое применение.
Событийный цикл
Идея событийного цикла очень проста. Есть бесконечный цикл, в котором движок JavaScript ожидает задачи, исполняет их и снова ожидает появления новых.
Общий алгоритм движка:
Это формализация того, что мы наблюдаем, просматривая веб-страницу. Движок JavaScript большую часть времени ничего не делает и работает, только если требуется исполнить скрипт/обработчик или обработать событие.
…Но, возможно, мы хотим что-нибудь показать во время выполнения задачи, например, индикатор выполнения.
Так будет красивее:
Пример 3: делаем что-нибудь после события
В обработчике события мы можем решить отложить некоторые действия, пока событие не «всплывёт» и не будет обработано на всех уровнях. Мы можем добиться этого, обернув код в setTimeout с нулевой задержкой.
Макрозадачи и Микрозадачи
Помимо макрозадач, описанных в этой части, существуют микрозадачи, упомянутые в главе Микрозадачи.
Сразу после каждой макрозадачи движок исполняет все задачи из очереди микрозадач перед тем, как выполнить следующую макрозадачу или отобразить изменения на странице, или сделать что-то ещё.
Какой здесь будет порядок?
Более подробное изображение событийного цикла выглядит так:
Все микрозадачи завершаются до обработки каких-либо событий или рендеринга, или перехода к другой макрозадаче.
Это важно, так как гарантирует, что общее окружение остаётся одним и тем же между микрозадачами – не изменены координаты мыши, не получены новые данные по сети и т.п.
Итого
Более подробный алгоритм событийного цикла (хоть и упрощённый в сравнении со спецификацией):
Чтобы добавить в очередь новую макрозадачу:
Этот способ можно использовать для разбиения больших вычислительных задач на части, чтобы браузер мог реагировать на пользовательские события и показывать прогресс выполнения этих частей.
Также это используется в обработчиках событий для отложенного выполнения действия после того, как событие полностью обработано (всплытие завершено).
Для добавления в очередь новой микрозадачи:
События пользовательского интерфейса и сетевые события в промежутках между микрозадачами не обрабатываются: микрозадачи исполняются непрерывно одна за другой.
Поэтому queueMicrotask можно использовать для асинхронного выполнения функции в том же состоянии окружения.
Для длительных тяжёлых вычислений, которые не должны блокировать событийный цикл, мы можем использовать Web Workers.
Это способ исполнить код в другом, параллельном потоке.
Web Workers могут обмениваться сообщениями с основным процессом, но они имеют свои переменные и свой событийный цикл.
Web Workers не имеют доступа к DOM, поэтому основное их применение – вычисления. Они позволяют задействовать несколько ядер процессора одновременно.
Задачи
Что код выведет в консоли?
Давайте разберем что здесь происходит.
Изначально в стеке выполнения находится сам скрипт, поэтому сначала выполняется только он.
Русские Блоги
JS макро задача, микро задача
Поэтому, если весь код выполняется синхронно, это вызовет очень серьезную проблему. Например, если мы хотим получить некоторые данные с удаленного конца, следует ли нам продолжать зацикливать код, чтобы судить, получили ли мы возвращаемый результат?Это все равно что ходить в ресторан за едой. Нельзя сказать, что тебя будут избивать, если ты пойдешь на кухню после того, как призовешь людей готовить.
Таким образом, существует концепция асинхронных событий: зарегистрируйте функцию обратного вызова, например, отправьте сетевой запрос, мы сообщим основной программе уведомить меня после получения данных, а затем мы можем выполнить другие вещь.
Затем, после асинхронного завершения, мы уведомим нас, но в это время программа может делать другие вещи, поэтому даже если асинхронное завершение завершено, вам нужно дождаться стороны и дождаться, пока программа не будет работать. Чтобы увидеть, какая асинхронность была завершена, вы можете выполнить ее.
Например, если вы столкнулись с автомобилем, если водитель прибывает первым, но у вас все еще есть с чем поработать, водитель не сможет сначала ехать в машине. Вы должны подождать, пока не сядете в машину после решения вопроса.
Разница между микрозадачами и макрозадачами
Это все равно что идти в банк, чтобы заниматься бизнесом.
Как правило, такие слова, как «Ваш номер XX, а перед ним XX человек».
Кроме того, во время выполнения макро-задачи вы можете добавить несколько микро-задач. Так же, как при ведении дел у прилавка, пожилой человек перед вами может внести депозит. После обработки депозита кассир попросит старика вернуть Есть ли другой бизнес, которым нужно заниматься? В это время старик думал об этом: «В последнее время очень много P2P-громов, вы хотите выбрать более стабильный финансовый менеджмент?» Конечно, я не могу сказать старику: «Ты поднимешь номер и вернешься в очередь».
Так что ваша очередь заниматься бизнесом, потому что старик временно добавил «Управление благосостоянием«И оттолкнул.
Может быть, старик все еще хочет сделатьПолучить другую кредитную карту? илиКупите еще памятные монеты?
Независимо от того, что вам нужно, если кассир может ей помочь, они будут делать это до того, как заняться вашим бизнесом. Это можно рассматривать как микро-задачи.
Это значит: твой дядя всегда будет твоим дядей
Если текущая микрозадача не завершена, следующая макрозадача не будет выполнена.
Итак, есть фрагмент кода, который часто используется в вопросах интервью и различных блогах:
setTimeout Существует как макро-задача, и Promise.then Это типичная микрозадача, и порядок выполнения вышеуказанного кода выводится в соответствии с серийным номером.
Все асинхронное, которое будет введено, относится к части кода в обратном вызове события.
означает new Promise Код, выполняемый в процессе создания экземпляра, синхронизируется, и then Зарегистрированный обратный вызов выполняется асинхронно.
После завершения выполнения синхронного кода вернитесь назад, чтобы проверить, завершена ли асинхронная задача, и выполните соответствующий обратный вызов, и микро-задача будет выполнена до выполнения макро-задачи.
Итак, я получил приведенный выше вывод 1、2、3、4 。
+ Часть представляет код, который выполняется синхронно
первоначально setTimeout Таймер был установлен (эквивалентно взятию числа), а затем некоторые из них добавляются в текущий процесс Promise Обработка (добавление бизнеса временно).
П.С. вОбещание / A + технические характеристикив Promise Реализация может быть микро-задачей или макро-задачей, но общее согласие указывает (по крайней мере, Chrome Это сделано), Promise Должен принадлежать к микро-миссионерскому лагерю
Макро задача
# | браузер | Node |
---|---|---|
I/O | ✅ | ✅ |
setTimeout | ✅ | ✅ |
setInterval | ✅ | ✅ |
setImmediate | ❌ | ✅ |
requestAnimationFrame | ✅ | ❌ |
Некоторые места будут перечислены UI Rendering Сказать, что это тоже макро-задача, но я читаюДокумент спецификации HTMLПозже выяснилось, что это явно шаг операции, параллельный микрозадаче.
requestAnimationFrame Давайте назовем это макрозадачей, requestAnimationFrame вОпределение MDNЧтобы выполнить операцию до перерисовки следующей страницы, перерисовка также существует как шаг макро-задачи, и этот шаг позже, чем выполнение микро-задачи
Микро-задачи
# | браузер | Node |
---|---|---|
process.nextTick | ❌ | ✅ |
MutationObserver | ✅ | ❌ |
Promise.then catch finally | ✅ | ✅ |
Что такое Event-Loop
Выше обсуждались макро-задачи, микро-задачи и выполнение различных задач.
Но вернемся к реальности, JavaScript Это однопроцессный язык, и он не может обрабатывать несколько задач одновременно, поэтому когда выполнять макро-задачи и когда выполнять микро-задачи? Нам нужна такая логика суждения.
Каждый раз после завершения бизнеса кассир спрашивает текущего клиента, есть ли какие-либо другие предприятия, которые необходимо обработать.(Проверьте, есть ли какие-либо микро-задачи для обработки)
И после того, как клиент четко сообщил, что ничего не было, кассир пошел, чтобы проверить, есть ли люди, которые ждут, чтобы заняться бизнесом.(Завершите эту макро-задачу, проверьте, есть ли какая-либо макро-задача для обработки)
Этот процесс проверки является непрерывным, и он будет выполняться после завершения каждой задачи, и такая операция называется Event Loop 。(Это очень простое описание, на самом деле это будет намного сложнее)
И, как уже упоминалось выше, кассир может обрабатывать только одну вещь за раз, даже если эти вещи предлагаются клиентом, поэтому можно считать, что существует очередь для микрозадач, примерно такая логика:
Причина использования двух for Цикл выражается потому, что он может быть легко выполнен внутри цикла push Такие операции (добавить некоторые задачи), так что количество итераций увеличивается динамически.
И чтобы быть ясно, Event Loop Он просто отвечает за то, чтобы сказать вам, какие задачи выполнять или какие обратные вызовы вызваны, реальная логика все еще выполняется в процессе.
Производительность в браузере
Выше кратко объясняется разница между этими двумя задачами, и Event Loop Роль, тогда какова производительность в реальном браузере?
Первое, что нужно прояснить, это то, что макро-задача должна выполняться после микро-задачи (потому что микро-задача на самом деле является одним из шагов макро-задачи)
Предположим, есть некоторые DOM Структура:
Некоторые небольшие сюрпризы
Это все равно что ходить в ресторан за едой. Официант три раза кричал. XX говяжья лапша не означает, что она даст вам три миски говяжьей лапши.
См вышеTasks, microtasks, queues and schedulesЕсть анимированная версия статьи
Производительность в узле
Узел также однопоточный, но обрабатывает Event Loop Браузер немного отличается от браузера, вотУзел официальной документацииАдрес.
Только на уровне API Node добавил два новых метода, которые можно использовать: микрозадача process.nextTick И макро задачи setImmediate 。
Разница между setImmediate и setTimeout
Определение в официальном документе, setImmediate На этот раз Event Loop Вызывается после завершения выполнения.
setTimeout Выполняется после расчета времени задержки.
Если вам интересно, вы можете поэкспериментировать самостоятельно. Если вы выполните это несколько раз, вы получите разные результаты.
Но если вы добавите код позже, вы можете гарантировать setTimeout Обязательно будет в setImmediate Срабатывает раньше:
Если в другой задаче макроса, она должна быть setImmediate Сначала выполните:
process.nextTick
Как уже упоминалось выше, это можно рассматривать как аналог Promise и MutationObserver Реализация микро-задачи может быть вставлена в любое время во время выполнения кода nextTick И будет гарантированно выполнено до начала следующей макро-задачи.
Одним из наиболее распространенных примеров использования является работа некоторых классов привязки событий:
Поскольку приведенный выше код создан Lib Объект выполняется синхронно и отправляется сразу после завершения создания экземпляра. init Событие.
и основная программа на внешнем уровне еще не начала выполняться lib.on(‘init’) Этот шаг прослушивания событий.
Следовательно, при отправке события обратного вызова не будет, и событие не будет отправлено снова после регистрации обратного вызова.
Мы можем легко использовать process.nextTick Чтобы решить эту проблему:
Это будет запущено, когда код основного процесса будет выполнен, и программа простаивает Event Loop Процесс, чтобы найти, если есть какие-либо микро задачи перед отправкой init Событие.
Визуализация промисов и Async/Await
Доброго времени суток, друзья!
Представляю вашему вниманию перевод статьи «JavaScript Visualized: Promises & Async/Await» автора Lydia Hallie.
Приходилось ли вам сталкиваться с JavaScript кодом, который… работает не так, как ожидается? Когда функции выполняются в произвольном, непредсказуемом порядке, или выполняются с задержкой. Одна из главных задач промисов — упорядочение выполнения функций.
Мое ненасытное любопытство и бессонные ночи окупились сполна — благодаря им я создала несколько анимаций. Пришло время поговорить о промисах: как они работают, почему их следует использовать и как это делается.
Введение
При написании JS кода нам часто приходится иметь дело с задачами, которые зависят от других задач. Допустим, мы хотим получить изображение, сжать его, применить к нему фильтр и сохранить его.
В результате получаем следующее:
Хм… Что-нибудь заметили? Несмотря на то, что все работает, выглядит это не лучшим образом. Мы получаем множество вложенных функций обратного вызова, зависящих от предыдущих коллбэков. Это называется адом коллбэков и это существенно усложняет чтение и поддержку кода.
К счастью, сегодня у нас есть промисы.
Синтаксис промисов
Промисы были представлены в ES6. Во многих руководствах вы можете прочитать следующее:
Промис (обещание) — это значение, которое выполняется или отклоняется в будущем.
Да уж… Так себе объяснение. В свое время оно заставило меня считать промисы чем-то странным, неопределенным, какой-то магией. Чем же они являются на самом деле?
Погодите, что здесь возвращается?
PromiseStatus или статус промиса может принимать одно из трех значений:
Посмотрим, что выводится в консоль при вызове методов resolve и reject :
[[PromiseValue]] или значением промиса является значение, которое мы передаем методам resolve или reject в качестве аргумента.
Во введении я привела пример, в котором мы получаем изображение, сжимаем его, применяем к нему фильтр и сохраняем его. Тогда все закончилось адом коллбэков.
К счастью, промисы помогают с этим справиться. Перепишем код так, чтобы каждая функция возвращала промис.
Если изображение загрузилось, выполняем промис. В противном случае, если произошла ошибка, отклоняем промис:
Посмотрим, что происходит при запуске этого кода в терминале:
Клево! Промис возвращается с разобранными («распарсенными») данными, как мы и ожидали.
Но… что дальше? Нас не интересует объект промиса, нас интересуют его данные. Для получения значения промиса существует 3 встроенных метода:
Наконец, мы получили искомое значение. Мы можем делать с этим значением все что угодно.
Когда мы уверены в выполнении или отклонении промиса, можно писать Promise.resolve или Promise.reject с соответствующим значением.
Именно такой синтаксис будет использоваться в следующих примерах.
Такой синтаксис выглядит гораздо лучше лестницы вложенных функций обратного вызова.
Микрозадачи и (макро)задачи
Хорошо, теперь мы знаем, как создавать промисы и как извлекать из них значения. Добавим немного кода в наш скрипт и запустим его снова:
Точно. В цикле событий (Event Loop) существует два типа очередей: очередь (макро)задач или просто задач ((macro)task queue, task queue) и очередь микрозадач или просто микрозадачи (microtask queue, microtasks).
Что относится к каждой из них? Если вкратце, то:
Сначала Task1 возвращает значение и удаляется из стека. Затем движок проверяет наличие микрозадач в соответствующей очереди. После добавления и последующего удаления из стека микрозадач, движок проверяет наличие макрозадач, которые также добавляются в стек и удаляются из него после возврата значений.
Довольно слов. Давайте писать код.
Движок видит, что стек пуст. Он «заглядывает» в очередь микрозадач. Она тоже пуста.
Готово. Теперь все встало на свои места, не правда ли?
Async/Await
В ES7 был представлен новый способ работы с асинхронным кодом в JS. С помощью ключевых слов async и await мы можем создать асинхронную функцию, неявно возвращающую промис. Но… как нам это сделать?
Получается, что мы можем отложить выполнение асинхронной функции? Отлично, но… что это значит?
Посмотрим, что происходит при запуске следующего кода:
После этого выполнение асинхронной функции откладывается. Выполнение тела функции приостанавливается, оставшийся код выполняется как микрозадача.
После того, как выполнение асинхронной функции было отложено, движок возвращается к выполнению кода в глобальном контексте.
Получилось довольно многословно. Не переживайте, если чувствуете себя неуверенно при работе с промисами. Для того, чтобы к ним привыкнуть требуется какое-то время. Это характерно для всех приемов работы с асинхронным кодом в JS.
Спасибо за потраченное время. Надеюсь оно было потрачено не зря.
Задачи, микрозадачи, очереди и планы
Предлагаю вашему вниманию перевод статьи «Tasks, microtasks, queues and schedules» Джейка Арчибальда (Jake Achibald), занимающего должность Developer Advocate for Google Chrome.
Когда я сказал своему коллеге Мэту Ганту, что подумываю о написании статьи об очерёдности микрозадач и порядке их исполнения внутри событийного цикла браузера, он сказал «Джейк, буду честен, я об этом читать не стану». Что ж, я всё же написал, поэтому откиньтесь на спинку кресла и давайте вместе в этом разберёмся, ладно?
На самом деле, если вам будет проще посмотреть видео, есть замечательное выступление Филиппа Робертса на JSConf, которое рассказывает о событийном цикле – оно не покрывает микрозадачи, но в остальном является отличным вступлением в тему. В любом случае, погнали…
Давайте рассмотрим следующий код на JavaScript:
Почему так происходит
Для более точного понимания процесса нужно сначала представить как событийный цикл обрабатывает задачи и микрозадачи. На первый раз это может показаться слишком сложным. Глубокий вдох…
Каждый «поток» имеет собственный событийный цикл, а значит и каждый веб-воркер, так что они могут выполняться независимо, тогда как все окна с одного домена (по правилу same origin) делят между собой один и тот же событийный цикл, ведь они могут синхронно коммуницировать между собой. Событийный цикл работает постоянно, исполняя поставленные в очередь задачи. Задачи выполняются последовательно и не могут пересекаться. Ладно-ладно, не уходите…
Задачи планируются таким образом чтобы браузер мог из их дебрей ступить на землю JavaScript/DOM и быть уверенным что эти действия происходят поочерёдно. Обработка колбека события щелчка мыши требует планирования задачи, так же как и разбор HTML и setTimeout из примера выше.
Микрозадачи обычно планируются для вещей, который должны исполняться моментально после текущего исполняемого сценария. Например, реагирование на пачку действий или для того, чтобы сделать что-то асинхронно без необходимости терять производительность на пустом месте из-за полностью новой задачи. Очередь микрозадач развёртывается в конце каждой полной задачи, а также после колбеков в случае если никакой другой JavaScript не находится на стадии исполнения. Любые дополнительные микрозадачи, поставленные в очередь во время развёртывания очереди микрозадач, добавляются в конец очереди и тоже обрабатываются. Микрозадачи включают в себя колбеки Mutation observer и промисов, как в примере выше.
Прим. переводчика: в этом месте у автора в оригинале вставлена великолепная наглядная презентация работы планироващика JavaScript, однако повторить это на Хабре у меня едва ли имеется техническая возможность, за сим отправляю любознательного читателя на страницу оригинала.
Да, я и правда сделал пошаговую анимированную диаграмму. Как вы провели свою субботу, наверняка гуляли где-то на свежем воздухе с друзьями? Что ж, я – нет. На случай, если что-то не ясно в моём обалденном UI, попробуйте пощёлкать стрелочки вправо-влево.
Что неправильно в некоторых браузерах?
Как понять когда используются задачи, а когда – микрозадачи
Точный способ – посмотреть спецификацию. Например, шаг 14 setTimeout ставит в очередь задачу, тогда как в спецификации фиксирования мутации шаг 5 создаёт микрозадачу.
Теперь давайте взглянем на более комплексный пример. В зале кто-то сконфужено вскрикнет «Нет, они не готовы!». Не обращайте внимания, вы готовы.
Первый уровень: Схватка с Боссом
Следующая задачка могла бы показаться мне сложной до того как я написал этот пост. Вот небольшой кусок HTML:
Попробуйте подумать прежде чем перейдёте к ответу. Подсказка: логи могут выводиться больше раза.
Испытание
Прим. переводчика: у автора в этом месте в блоге есть интерактивный DOM элемент (прямая ссылка) на котором можно воочию проверить поведение вашего браузера.
Вы думали будет иначе? Спешу вас успокоить, возможно вы были правы. К сожалению, у разных браузеров разная степень приятия этого мнения:
Кто прав?
Обработка события «click» это задача. Колбеки Mutation observer и промиса ставятся в очередь как микрозадачи. Колбек setTimeout это задача. (Прим. переводчика: тут снова интерактивная диаграмма, поясняющая пошагово принцип работы приведённого ранее кода, рекомендую взглянуть.)
Так что правильно ведёт себя Chrome. Для меня в новость было узнать что микрозадачи развёртываются после колбеков (если только это не часть выполнения другого сценария JavaScript), я думал что их развёртывание ограничено лишь окончанием выполнения задачи. Это правило описано в спецификации HTML по вызову колбеков:
Execution of a Job can be initiated only when there is no running execution context and the execution context stack is empty…
— ECMAScript: Jobs and Job Queues
…хотя «can be» в контексте HTML носит характер «must be», т.е. «обязан».
Что недопоняли браузеры?
Firefox и Safari верно опустошают очередь микрозадач между обработчиками щелчков, как видно по колбекам мутации, но промисы ставятся в очередь иначе. Это можно было бы простить, особенно учитывая туманность связи между заданием («jobs») и микрозадачей, однако я ожидал что они выполнятся между обработчиками. Заявка на Firefox. Заявка на Safari.
Мы уже поняли, что Edge ставит промисы в очередь неверно, но он также не стал опустошать очередь микрозадач между обработчиками щелчков, вместо этого очередь развернулась лишь после вызова всех обработчиков, что объясняет единственный вывод mutate после обоих click в журнале. Это ошибка.
Злой брат Босса с Первого уровня
Блин! А что если к предыдущему примеру добавить:
Событие начнёт обрабатываться точно так же как и до этого, но посредством вызова из сценария, а не от реального взаимодействия пользователя.
Испытание
И я не перестаю получать различные результаты в Chrome, я уже сто раз обновлял эту таблицу думая что до этого по ошибке проверял в Canary. Если у вас в Chrome другие результаты, скажите мне в комментариях на какой вы версии.
Почему теперь по-другому?
Прим. переводчика: в этом месте ещё один последний раз автор даёт нам возможность насладиться визуализацией чудес инженерной мысли браузеростроителей (ссылка, опять-таки, прямая).
После того как каждый из обработчиков щелчка вызван…
Разве это важно?
Ещё бы, это будет съедать вас изнутри (уф). Я столкнулся с этим когда попытался создать лаконичную обёртку над IndexedDB, использующую промисы вместо ужасных объектов IDBRequest. С ней IDB почти стал мне приятен.
Когда в IDB срабатывает событие успешности, объект транзакции становится неактивным после передачи управления (шаг 4). Если я создам промис, который решается во время возбуждения этого события, обработчики должны бы исполниться до шага 4 пока транзакция ещё активна, однако этого не происходит ни в одном браузере кроме Chrome, из-за чего библиотека становится как бы бесполезной.
В Firefox с этим можно справиться, ведь полифилы промисов, такие как es6-promise, используют Mutation observers для колбеков, которые есть не что иное как микрозадачи. Safari при этом исправлении вступает в состояние гонки, но дело, скорее всего, в их поломанной реализации IDB. К сожалению IE/Edge на данный момент не подлежит исправлению, так как события мутаций не происходят после колбеков.
Остаётся лишь надеяться что в этом вопросе мы когда-то сможем наблюдать взаимозаменяемость.