Разница между микрозадачей и макрозадачей в контексте цикла событий

140

Я только что закончил читать Обещания / A + спецификации и наткнулся на термины microtask и macrotask: см http://promisesaplus.com/#notes

Я никогда не слышал об этих терминах раньше, и теперь мне любопытно, какая разница?

Я уже пытался найти некоторую информацию в Интернете, но все, что я нашел, - это сообщение из архива w3.org (которое не объясняет мне разницу): http://lists.w3.org/Archives /Public/public-nextweb/2013Jul/0018.html

Кроме того, я нашел модуль npm под названием «macrotask»: https://www.npmjs.org/package/macrotask Опять же, не ясно, в чем именно заключается разница.

Все, что я знаю, это то, что он как-то связан с циклом событий, как описано в https://html.spec.whatwg.org/multipage/webappapis.html#task-queue и https: //html.spec.whatwg. .org / многостраничный / webappapis.html # выполнить-а-microtask-пропускной пункт

Я знаю, что теоретически я должен быть в состоянии извлечь различия самостоятельно, учитывая эту спецификацию WHATWG. Но я уверен, что другие могли бы также извлечь выгоду из краткого объяснения, данного экспертом.

NicBright
источник
Короче говоря: несколько вложенных очередей событий. Вы могли бы даже реализовать это самостоятельно:while (task = todo.shift()) task();
Bergi
1
Для кого-то, кто хочет немного больше деталей: Секреты ниндзя JavaScript, 2-е издание, ГЛАВА 13 Выжившие события
Этан

Ответы:

220

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

Каковы практические последствия этого?

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

Однако, по крайней мере, в отношении функции process.nextTick Node.js (которая ставит в очередь микрозадачи ), существует встроенная защита от такой блокировки с помощью process.maxTickDepth. Для этого значения по умолчанию установлено значение 1000, что сокращает дальнейшую обработку микрозадач после достижения этого предела, что позволяет обрабатывать следующую макрозадачу )

Итак, когда использовать что?

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

Примеры

макрозадачи: setTimeout , setInterval , setImmediate , requestAnimationFrame , I / O ,
микрозадачи рендеринга UI: process.nextTick , Promises , queueMicrotask , MutationObserver

NicBright
источник
4
Хотя в цикле обработки событий есть контрольная точка для микрозадач, большинство разработчиков не сталкиваются с ней. Микрозадачи обрабатываются при опустошении стека JS. Это может происходить много раз внутри задачи или даже внутри шагов рендеринга цикла событий.
JaffaTheCake
2
process.maxTickDepthбыл удален очень давно: github.com/nodejs/node/blob/…
RidgeA
Вы также можете использовать метод queueMicrotask () для добавления новой микрозадачи
ZoomAll
Спасибо @ZoomAll, до сих пор не знал queueMicrotask (). Я добавил это к ответу плюс ссылки на все вещи ...
NicBright
requestAnimationFrame (rAF) не только генерирует микрозадачи. Вообще говоря, вызов rAF создает отдельную очередь
ZoomAll
67

Основные понятия в спецификации :

  • Цикл событий имеет одну или несколько очередей задач (очередь задач - очередь макросов).
  • Каждый цикл событий имеет очередь микрозадач.
  • очередь задач = очередь макрозадач! = очередь микрозадач
  • задача может быть помещена в очередь макрозадач или в очередь микрозадач
  • когда задача помещается в очередь (микро / макро), мы имеем в виду, что подготовка к работе завершена, поэтому задача может быть выполнена сейчас.

А модель процесса цикла событий выглядит следующим образом:

когда стек вызовов пуст, выполните следующие действия:

  1. выбрать самую старую задачу (задача A) в очередях задач
  2. если задача A пуста (означает, что очереди задач пусты), перейдите к шагу 6
  3. установить "текущая задача" на "задачу A"
  4. запустить «задачу A» (означает запустить функцию обратного вызова)
  5. установить "текущая задача" на null, удалить "задачу A"
  6. выполнить очередь микрозадач
    • (а). выбрать самую старую задачу (задача x) в очереди микрозадач
    • (b). если задача x равна нулю (означает, что очереди микрозадач пусты), перейти к шагу (g)
    • (c). установить "текущая задача" на "задача x"
    • (d). запустить "task x"
    • (e). установить "текущая задача" в null, удалить "задачу x"
    • (f). выберите следующую самую старую задачу в очереди для микрозадач, перейдите к шагу (b)
    • (g) очередь завершения микрозадач;
  7. перейти к шагу 1.

упрощенная модель процесса выглядит следующим образом:

  1. запустите самую старую задачу в очереди макросов, затем удалите ее.
  2. запустите все доступные задачи в очереди микропроцессоров, затем удалите их.
  3. следующий раунд: выполнить следующую задачу в очереди макротестов (шаг 2 перехода)

что запомнить

  1. при выполнении задачи (в очереди макрозадач) могут регистрироваться новые события, поэтому могут создаваться новые задачи. Ниже приведены две новые созданные задачи:
    • Обратный вызов обещания A.then () - это задача
      • обещаниеA разрешено / отклонено: задача будет помещена в очередь микрозадач в текущем раунде цикла событий.
      • PromiseA ожидает выполнения: задача будет помещена в очередь микрозадач в следующем раунде цикла событий (может быть в следующем раунде)
    • Обратный вызов setTimeout (callback, n) является задачей и будет помещен в очередь макрозадач, даже если n равно 0;
  2. задача в очереди микрозадач будет запущена в текущем раунде, а задача в очереди макрозадач должна ожидать следующего раунда цикла событий.
  3. мы все знаем, что обратные вызовы "click", "scroll", "ajax", "setTimeout" ... являются задачами, однако мы также должны помнить, что js-коды в целом в теге script тоже являются задачей (макротаска).
wengeezhang
источник
2
Это отличное объяснение! Спасибо, что поделился!. Еще одна вещь, которую нужно упомянуть, в NodeJs , setImmediate()это макрос / задача и process.nextTick()микро / работа.
LeOn - Хан Ли
6
А как насчет paintзадач браузера ? В какую категорию они бы вписались?
Legends
Я думаю, они подошли бы к requestAnimationFrame
микрозадачам
Вот порядок, в котором запускается цикл событий v8 -> Стек вызовов || Микро Задачи || Очередь задач || rAF || Render Tree || Макет || Краска || <Собственные вызовы OS для рисования пикселей на экране> <----- 1) DOM (новые изменения), CSSOM (новые изменения), дерево рендеринга, макет и рисование происходят после обратного вызова requestAnimationFrame в соответствии с таймерами цикла событий. Вот почему так важно как можно чаще завершать операции с DOM перед rAF, остальное можно сделать в rAF. PS: вызов rAF вызовет выполнение макро-задачи.
Анвеш Чека
Не знаю, ошибаюсь ли я, но я вроде как не согласен с этим ответом, микрозадачи выполняются до макрозадач. codepen.io/walox/pen/yLYjNRq ?
Walox
9

Я думаю, что мы не можем обсуждать цикл обработки событий при отделении от стека, поэтому:

JS имеет три «стека»:

  • стандартный стек для всех синхронных вызовов (одна функция вызывает другую и т. д.)
  • очередь микрозадач (или очередь заданий, или стек микрозадач) для всех асинхронных операций с более высоким приоритетом (process.nextTick, Promises, Object.observe, MutationObserver)
  • очередь макрозадач (или очередь событий, очередь задач, очередь макрозадач) для всех асинхронных операций с более низким приоритетом (setTimeout, setInterval, setImmediate, requestAnimationFrame, I / O, рендеринг пользовательского интерфейса)
|=======|
| macro |
| [...] |
|       |
|=======|
| micro |
| [...] |
|       |
|=======|
| stack |
| [...] |
|       |
|=======|

И цикл обработки событий работает следующим образом:

  • выполнять все снизу вверх из стека, и ТОЛЬКО когда стек пуст, проверять, что происходит в очередях выше
  • проверить микростек и выполнить все там (при необходимости) с помощью стека, одну микрозадачу за другой, пока очередь микрозадач не станет пустой или не потребует выполнения, и ТОЛЬКО затем проверьте стек макросов
  • проверить стек макросов и выполнить все там (если требуется) с помощью стека

Стек Mico не будет трогаться, если стек не пуст. Макропакет не будет сенсорным, если микропогрузка не пуста ИЛИ не требует выполнения.

Подводя итог: очередь микрозадач почти такая же, как очередь макрозадач, но эти задачи (process.nextTick, Promises, Object.observe, MutationObserver) имеют более высокий приоритет, чем макрозадачи.

Микро как макро, но с более высоким приоритетом.

Вот вам и «окончательный» код для понимания всего.

console.log('stack [1]');
setTimeout(() => console.log("macro [2]"), 0);
setTimeout(() => console.log("macro [3]"), 1);

const p = Promise.resolve();
for(let i = 0; i < 3; i++) p.then(() => {
    setTimeout(() => {
        console.log('stack [4]')
        setTimeout(() => console.log("macro [5]"), 0);
        p.then(() => console.log('micro [6]'));
    }, 0);
    console.log("stack [7]");
});

console.log("macro [8]");

/* Result:
stack [1]
macro [8]

stack [7], stack [7], stack [7]

macro [2]
macro [3]

stack [4]
micro [6]
stack [4]
micro [6]
stack [4]
micro [6]

macro [5], macro [5], macro [5]
--------------------
but in node in versions < 11 (older versions) you will get something different


stack [1]
macro [8]

stack [7], stack [7], stack [7]

macro [2]
macro [3]

stack [4], stack [4], stack [4]
micro [6], micro [6], micro [6]

macro [5], macro [5], macro [5]

more info: https://blog.insiderattack.net/new-changes-to-timers-and-microtasks-from-node-v11-0-0-and-above-68d112743eb3
*/
user1660210
источник
1
Вызов очереди из стека полностью сбивает с толку.
Берги