Разница между микрозадачей и макрозадачей в контексте цикла событий
Я только что закончил читать спецификацию Promises/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 и https: //html.spec.whatwg..org / многостраничный / webappapis.html # выполнить-а-microtask-пропускной пункт
Я знаю, что теоретически я должен быть в состоянии извлечь различия самостоятельно, учитывая эту спецификацию WHATWG. Но я уверен, что другие могли бы также извлечь выгоду из краткого объяснения, данного экспертом.
8 ответов
В одном цикле обработки событий будет ровно одна задача, обрабатываемая из очереди макросов (эта очередь в спецификации WHATWG называется просто очередью задач). После завершения этой макрозадачи все доступные микрозадачи будут обработаны, а именно в рамках одного цикла обработки. В то время как эти микрозадачи обрабатываются, они могут ставить в очередь еще больше микрозадач, которые будут выполняться одна за другой до тех пор, пока очередь микрозадач не будет исчерпана.
Каковы практические последствия этого?
Если микрозадача рекурсивно ставит в очередь другие микрозадачи, может пройти много времени, прежде чем будет обработана следующая макрозадача. Это означает, что в вашем приложении может появиться заблокированный пользовательский интерфейс или несколько завершенных операций ввода-вывода.
Однако, по крайней мере, в отношении функции process.nextTick Node.js (которая ставит в очередь микрозадачи), существует встроенная защита от такой блокировки с помощью process.maxTickDepth. Для этого значения по умолчанию установлено значение 1000, что сокращает дальнейшую обработку микрозадач после достижения этого предела, что позволяет обрабатывать следующую макрозадачу)
Итак, когда использовать что?
В основном, используйте микрозадачи, когда вам нужно делать что-то асинхронно синхронно (то есть когда вы говорите, что выполняете эту (микро) задачу в ближайшем будущем). В противном случае, придерживайтесь макрозадач.
Примеры
макрозадачи: setTimeout, setInterval, setImmediate, requestAnimationFrame, I / O, рендеринг пользовательского интерфейса
микротрубы: process.nextTick, Promises, Object.observe, MutationObserver
Основные понятия в спецификации:
- Цикл событий имеет одну или несколько очередей задач (очередь задач - очередь макросов)
- Каждый цикл событий имеет очередь микрозадач.
- очередь задач = очередь макрозадач!= очередь микрозадач
- задача может быть помещена в очередь макрозадач или в очередь микрозадач
- когда задача помещается в очередь (микро / макро), мы имеем в виду, что подготовка к работе завершена, поэтому задача может быть выполнена сейчас.
И модель процесса цикла событий выглядит следующим образом:
когда стек вызовов пуст, выполните шаги
- выберите самую старую задачу (задачу A) в очередях задач
- если задача A пуста (означает, что очереди задач пусты), перейдите к шагу 6
- установите "текущее задание" на "задание А"
- запустить "задачу A" (означает запустить функцию обратного вызова)
- установите "текущее задание" на ноль, удалите "задание А"
- выполнить очередь микрозадач
- (а).Выберите самую старую задачу (задачу х) в очереди микрозадач.
- (b). если задача x равна нулю (означает, что очереди в микрозадачах пусты), перейдите к шагу (g)
- (c).set "текущее задание" - "задание x"
- (d).run "задача x"
- (e).set "текущая задача" в null, удалить "задачу x"
- (f). выберите следующую самую старую задачу в очереди для микрозадач, перейдите к шагу (b)
- (g).финишная очередь микрозадач;
- перейти к шагу 1
упрощенная модель процесса выглядит следующим образом:
- запустите самую старую задачу в очереди макросов, затем удалите ее.
- запустите все доступные задачи в очереди микропроцессоров, затем удалите их.
- следующий раунд: запустить следующую задачу в очереди макротест (шаг 2)
что запомнить
- когда задача (в очереди макросов) выполняется, могут регистрироваться новые события. Таким образом, могут создаваться новые задачи. Ниже представлены две новые созданные задачи:
- Обратный вызов promain.. () является задачей
- Обещание acceptA разрешено / отклонено: задача будет помещена в очередь для микрозадач в текущем цикле цикла обработки событий.
- Обещание об ожидаемой версии: задача будет помещена в очередь для микрозадач в следующем цикле цикла событий (может быть в следующем раунде)
- Функция обратного вызова setTimeout(callback,n) является задачей и будет помещена в очередь макросов, даже если n равно 0;
- Обратный вызов promain.. () является задачей
- задача в очереди микрозадач будет запущена в текущем раунде, а задача в очереди макрозадач должна ждать следующего цикла цикла событий.
- мы все знаем, что обратные вызовы "click","scroll","ajax","setTimeout"... являются задачами, однако мы также должны помнить, что js-коды в целом в теге script тоже являются задачей (макротаска).
Я думаю, что мы не можем обсуждать цикл событий отдельно от стека, поэтому:
JS имеет три "стека":
- стандартный стек для всех синхронных вызовов (одна функция вызывает другую и т. д.)
- очередь микрозадач (или очередь заданий, или стек микрозадач) для всех асинхронных операций с более высоким приоритетом (process.nextTick, Promises, Object.observe, MutationObserver)
- очередь макрозадач (или очередь событий, очередь задач, очередь макрозадач) для всех асинхронных операций с более низким приоритетом (setTimeout, setInterval, setImmediate, requestAnimationFrame, I/O, рендеринг пользовательского интерфейса)
|=======|
| macro |
| [...] |
| |
|=======|
| micro |
| [...] |
| |
|=======|
| stack |
| [...] |
| |
|=======|
И цикл событий работает так:
- выполнять все снизу вверх из стека, и ТОЛЬКО когда стек пуст, проверять, что происходит в очередях выше
- проверить микростек и выполнить все там (при необходимости) с помощью стека, одну микрозадачу за другой, пока очередь микрозадач не станет пустой или не потребует выполнения, и ТОЛЬКО затем проверьте стек макросов
- проверить стек макросов и выполнить все там (если требуется) с помощью стека
Стек Mico не будет трогаться, если стек не пуст. Стек макросов не будет задействован, если микростек не пуст ИЛИ не требует выполнения.
Подводя итог: очередь микрозадач почти такая же, как очередь макрозадач, но эти задачи (process.nextTick, Promises, Object.observe, MutationObserver) имеют более высокий приоритет, чем макрозадачи.
Micro похож на макрос, но с более высоким приоритетом.
Вот вам и "окончательный" код для понимания всего.
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
*/
Я написал сообщение на эту тему, включая интерактивные примеры https://jakearchibald.com/2015/tasks-microtasks-queues-and-schedules/
Обновление: я также выступил с докладом об этом https://www.youtube.com/watch?v=cCOL7MC4Pl0. В докладе более подробно рассказывается, как задачи и микрозадачи взаимодействуют с рендерингом.
Задачи макроса включают события клавиатуры, события мыши, события таймера, сетевые события, анализ HTML, изменение Urletc. Макрозадача представляет собой некоторую дискретную и независимую работу.
Микрозадачи — это небольшие задачи, которые обновляют состояние приложения и должны выполняться до того, как браузер продолжит выполнение других задач, таких как повторный рендеринг пользовательского интерфейса. Микрозадачи включают обратные вызовы обещаний и изменения мутации DOM. Микрозадачи позволяют нам выполнять определенные действия перед повторным рендерингом пользовательского интерфейса, тем самым избегая ненужного рендеринга пользовательского интерфейса, который может отображать несогласованное состояние приложения.
Разделение макро- и микрозадач позволяет циклу обработки событий расставлять приоритеты по типам задач; например, отдавая приоритет задачам, чувствительным к производительности.
За одну итерацию цикла обрабатывается не более одной макрозадачи (остальные остаются в очереди), тогда как обрабатываются все микрозадачи.
Обе очереди задач размещаются вне цикла событий, чтобы указать, что действие добавления задач в соответствующие им очереди происходит вне цикла событий. В противном случае любые события, происходящие во время выполнения кода JavaScript, будут игнорироваться. Действия по обнаружению и добавлению задач выполняются отдельно от цикла событий.
Оба типа задач выполняются по очереди. Когда задача начинает выполняться, она выполняется до своего завершения. Только браузер может остановить выполнение задачи; например, если задача занимает слишком много времени или памяти.
Все микрозадачи должны выполняться до следующего рендеринга, потому что их цель — обновить состояние приложения до того, как произойдет рендеринг.
Браузер обычно пытается отобразить страницу 60 раз в секунду. Принято считать, что 60 кадров в секунду — это частота, при которой анимация выглядит плавной. если мы хотим добиться бесперебойной работы приложений, одна задача и все микрозадачи, сгенерированные этой задачей, в идеале должны выполняться в течение 16 мс. Если задача выполняется дольше пары секунд, браузер показывает сообщение «Скрипт не отвечает».
Я создал псевдокод цикла событий, следуя 4 концепциям:
setTimeout, setInterval, setImmediate, requestAnimationFrame, I/O, визуализация пользовательского интерфейса являются частью очереди макрозадач. Сначала будет обработан один элемент макрозадачи.
process.nextTick, Promises, Object.observe, MutationObserver являются частью очереди микрозадач. Цикл событий обработает все элементы в этой очереди, включая один, который был обработан во время текущей итерации.
Существует еще одна очередь, называемая очередью анимации, которая содержит элементы задачи изменения анимации, которые будут обрабатываться дальше. Все задачи, находящиеся в этой очереди, будут обработаны (не включая новые, которые были добавлены в ходе текущей итерации). Он будет вызван, если пришло время для рендеринга
Конвейер рендеринга будет пытаться рендерить 60 раз в секунду (каждые 16 мс).
while (true){ // 1. Get one macrotask (oldest) task item task = macroTaskQueue.pop(); execute(task); // 2. Go and execute microtasks while they have items in their queue (including those which were added during this iteration) while (microtaskQueue.hasTasks()){ const microTask = microtaskQueue.pop(); execute(microTask); } // 3. If 16ms have elapsed since last time this condition was true if (isPaintTime()){ // 4. Go and execute animationTasks while they have items in their queue (not including those which were added during this iteration) const animationTasks = animationQueue.getTasks(); for (task in animationTasks){ execute(task); } repaint(); // render the page changes (via the render pipeline) } }
JavaScript - это высокоуровневый однопоточный язык с интерпретацией. Это означает, что ему нужен интерпретатор, который преобразует код JS в машинный код. интерпретатор означает двигатель. Двигатели V8 для хрома и webkit для сафари. Каждый движок содержит память, стек вызовов, цикл событий, таймер, веб-API, события и т. Д.
Цикл событий: микрозадачи и макрозадания
Концепция цикла событий очень проста. Существует бесконечный цикл, в котором движок JavaScript ожидает задач, выполняет их, а затем засыпает, ожидая новых задач.
Задачи установлены - движок обрабатывает их - затем ожидает новых задач (в спящем режиме и почти нулевом потреблении ЦП). Может случиться так, что задача приходит, когда движок занят, а затем ставится в очередь. Задачи образуют очередь, так называемую « очередь макрозадач ».
Микрозадачи исходят исключительно из нашего кода. Обычно они создаются с помощью обещаний: выполнение обработчика .then / catch / finally становится микрозадачей. Микрозадачи также используются «под прикрытием» await, поскольку это еще одна форма обработки обещаний. Сразу после каждой макрозадачи движок выполняет все задачи из очереди микрозадач до запуска любых других макрозадач, рендеринга или чего-либо еще.
Ответ на ваш вопрос можно найти в разделе «Задачи и микрозадачи» статьи MDN « Подробно: микрозадачи и среда выполнения JavaScript» :
Разница между очередью задач и очередью микрозадач проста, но очень важна:
- При выполнении задач из очереди задач среда выполнения выполняет каждую задачу, находящуюся в очереди в момент начала новой итерации цикла событий. Задачи, добавленные в очередь после начала итерации, не будут выполняться до следующей итерации.
- Каждый раз, когда задача завершается и стек контекста выполнения пуст, каждая микрозадача в очереди микрозадач выполняется одна за другой. Разница в том, что выполнение микрозадач продолжается до тех пор, пока очередь не опустеет, даже если на это время запланированы новые. Другими словами, микрозадачи могут ставить в очередь новые микрозадачи, и эти новые микрозадачи будут выполняться до начала выполнения следующей задачи и до окончания текущей итерации цикла событий.
«Контекст выполнения» относится к ответу пользователя user1660210 выше .