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

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

Оригинал от меняGitHub blog, добро пожаловать, чтобы следовать

предисловие

Все мы знаем, что JavaScript — это однопоточный язык, а это значит, что на одно и то же событие может выполняться только одна задача, а после окончания может выполняться следующая. Если предыдущее задание не выполнено, последнее задание будет ждать вечно. Только представьте, есть длинный сетевой запрос, если всем задачам нужно дождаться завершения этого запроса перед продолжением, это явно неразумно и мы не сталкивались с такой ситуацией в браузере (если только вы не хотите запрашивать Ajax синхронно) Причина в том, что JavaScript реализует планирование задач с помощью асинхронного механизма.

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

Давайте сначала рассмотрим вопрос интервью:

try {
  setTimeout(() => {
    throw new Error("Error - from try statement");
  }, 0);
} catch (e) {
  console.error(e);
}

Что выведет приведенный выше пример? ответ:

image

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

Если я изменю пример

console.log("A");

setTimeout(() => {
  console.log("B");
}, 100);

console.log("C");

Учащиеся, которые немного разбираются в асинхронном механизме в браузере, могут ответить, что будет выведено "A C B". В этой статье будет проанализирован цикл обработки событий, чтобы разобраться с асинхронностью в браузере и прояснить вышеуказанные проблемы.

стек вызовов

Стек вызова функции на самом деле является стеком контекста выполнения (стеком контекста выполнения), он будет создавать новый контекст выполнения всякий раз, когда вызывается функция, и вновь созданный контекст выполнения будет помещаться в стек контекста выполнения.

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

  1. Однопоточный (это определяется движком JavaScript).
  2. Синхронное выполнение (он всегда будет синхронно выполнять функцию поверх стека).
  3. Существует только один глобальный контекст.
  4. Контекстов функций может быть бесконечное количество (теоретически ограничений на контексты функций нет, но слишком много вызовет разрыв стека).
  5. Каждый вызов функции создает новую执行上下文, даже для рекурсивных вызовов.

Первое, что нужно здесь уточнить, это то, что стек выполнения контекста функции — это концепция, связанная с движком JavaScript (Engine), а асинхронность/обратный вызов — это концепция, связанная со средой выполнения (Runtime).

Если стек выполнения не имеет ничего общего с асинхронным механизмом, как мы можем это сделать, записывая обратные вызовы, инициируемые кликом, бесчисленное количество раз? Это делает среда выполнения (браузер/узел), в браузере реализован асинхронный механизм с помощью цикла обработки событий.цикл событий — это механизм реализации асинхронного. Механизм JavaScript просто «глупый» всегда выполняет функции на вершине стека, а среда выполнения отвечает за управление тем, когда и какие функции помещаются в стек контекста выполнения для выполнения движком.

Сам движок JavaScript не имеет понятия о времени, это просто среда, которая выполняет произвольные фрагменты кода JavaScript по требованию. Отправка событий (выполнение кода JavaScript) всегда выполняется содержащей средой.

Кроме того, с одной стороны, можно отразить, что стек контекста выполнения не имеет ничего общего с асинхронностью — стек контекста выполнения написан наECMA-262В спецификации необходимо соблюдать движок JavaScript браузера, такой как V8, Quantum и т. д. Цикл событий написан наHTMLВ спецификации все браузеры, такие как Chrome, Firefox и т. д., должны соблюдать ее.

event loop

определение

мы проходимСпецификация HTML5Посмотрите на определение цикла событий. Посмотрите на модель, все ссылки в этой главе переведены из спецификации.

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

Эта статья посвящена только браузерной части, поэтому игнорируйте Web Worker. Движок JavaScript не работает независимо, он должен работать в хост-среде, поэтому на самом деле лучший перевод пользовательского агента (user agent) в этом контексте должен бытьРабочая средаилихост-среда, который является браузером.

Каждый пользовательский агент должен иметь по крайней мере одинbrowsing context event loop, но каждыйunit of related similar-origin browsing contextsВы можете иметь только один.

оunit of related similar-origin browsing contexts, выдержка из введения части спецификации:

Each unit of related browsing contexts is then further divided into the smallest number of groups such that every member of each group has an active document with an origin that, through appropriate manipulation of the document.domain attribute, could be made to be same origin-domain with other members of the group, but could not be made the same as members of any other group. Each such group is a unit of related similar-origin browsing contexts.

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

Для чего нужен цикл событий?

каждыйevent loopиметь одну или несколько очередей задач.task queueпредставляет собой упорядоченный список задач, используемый дляоткликСледующий алгоритм работает следующим образом:

  • мероприятие

    существуетEventTargetПубликация события при срабатыванииEventобъект, это обычно делается специальной задачей.

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

  • Разобрать

    парсер HTMLПроцесс токенизации и генерации токенов — типичная задача.

  • Перезвоните

    Обычно для вызова функции обратного вызова используется конкретная задача.

  • Использование ресурсов

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

  • Реакция на манипуляции с DOM

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

Как видите, на странице есть только один цикл событий, но цикл событий может иметь несколько очередей задач.

каждая из одного и того же источника задач и одним и тем жеevent loop(Например,DocumentФункция обратного вызова, сгенерированная таймером,Documentсобытия, генерируемые движением мыши,Documentзадачи, сгенерированные парсером) управляемыеtaskнеобходимо добавить туда жеtask queue, но из разныхtask sourcesизtasks можно сортировать по разным task queuesсередина.

Задачи из одного и того же источника задач будут помещены в одну и ту же очередь задач, но в спецификации сказано, что из разныхtask sourcesизtasks возможнобудут рассортированы по разнымtask queuesДругими словами, задачи из разных источников задач могут быть расположены в очереди задач, но в спецификации не указано, какой источник задач соответствует какой очереди задач.

Но спецификация классифицирует источник задачи:

следующим образомtask sourcesШироко используется в этой спецификации или других функциях, не относящихся к спецификации:

  • источник задачи для манипулирования DOM

    этоtask sourceИспользуется для реагирования на операции DOM, такие какinserted into the documentнеблокирующее поведение.

  • Источник задачи действия пользователя

    этоtask sourceИспользуется для реагирования на реакции пользователя, такие как события мыши и клавиатуры. Эти события, которые используются для ответа на ввод пользователя, должны бытьuser interaction task sourceзапускать и ставить в очередьtasks queued.

  • источник сетевой задачи

    этоtask sourceОтветы, используемые для отражения сетевой активности.

  • источник задачи путешествия во времени

    этоtask sourceиспользовал кhistory.back()Подождите, пока API будет поставлен в очередь задач.

Как правило, мы видим, что в каждой статье есть только одно описание очереди задач, будь то сеть, пользовательское время или таймер, веб-API будут ставить их в очередь задач, но на самом деле в спецификации четко указано, что есть очередь задач Несколько очередей задач и иллюстрируют важность этого дизайна:

Например, пользовательский агент может иметь метод, который обрабатывает события клавиатуры и мыши.task queue(отuser interaction task source) и очередь задач для обработки всех остальных. Пользовательский агент может сначала обрабатывать события мыши и клавиатуры с вероятностью 75%, поэтому не гарантируется, что пользовательский интерфейс будет реагировать без выполнения других очередей задач вообще, и чтоtask sourceПоследовательность событий нарушена.

Тогда смотри.

когдапользовательский агентПри постановке задачи в очередь задача должна быть поставлена ​​в очередь в связанномevent loopизtask queues.

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

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

модель обработки

Мы можем визуализировать цикл событий как существование следующих форм:

while (queue.waitForMessage()) {
  queue.processNextMessage();
}

Цикл событий будет постоянно извлекать функции из очередей задач для выполнения, когда существует вся страница. Конкретные правила таковы:

Один event loopШаги, которые необходимо повторять снова и снова, пока он существует:

  1. Берем первую добавленную задачу событийного цикла из очередей задач, если нет задачи на выбор, то переходим к первойMicrotasksшаг.
  2. Установите задачу, выполняемую в данный момент циклом обработки событий, на задачу, выбранную на предыдущем шаге.
  3. 执行: выполнить выбранную задачу.
  4. Устанавливает для текущей выполняемой задачи цикла событий значение null.
  5. Удалить только что выполненную задачу из очереди задач.
  6. Microtasks:Задачи, выполняющие контрольные точки микрозадач.
  7. Обновите рендеринг, если это цикл событий в среде браузера (условно говоря, цикл событий в Worker), то выполните следующие шаги:
  8. Если это цикл событий в рабочей среде (например, вWorkerGlobalScopeвыполняется в цикле событий), но в очередях задач цикла событий нет задач иWorkerGlobalScopeОбъект является закрытым знаком, затем уничтожьте цикл событий, прекратите выполнение этих шагов и восстановите доrun a workerШаг.
  9. Вернитесь к шагу 1.

microtask

Спецификация приводит к микрозадаче,

каждыйevent loopЕсть очередь микрозадач. микрозадача - это своего родаmicrotask queueвместоtask queueзадача. Есть дваmicrotasks: одиночные микрозадачи обратного вызова и составные микрозадачи.

Спецификация описывает толькоsolitary callback microtasks, составные микрозадачи можно игнорировать в первую очередь.

когдаmicrotaskЧтобы быть поставленным в очередь, он должен быть поставлен в очередь соответствующим образомevent loopизmicrotask queue,microtaskизtask sourceявляется источником задачи микрозадачи.

контрольная точка микрозадач

Когда пользовательский агент достигает контрольной точки микрозадач, еслиperforming a microtask checkpoint flagложно, пользовательский агент должен выполнить следующие шаги:

  1. Будуperforming a microtask checkpoint flagустановить правду.

  2. 处理 microtask queue: если очередь микрозадач цикла событий пуста, сразу переходите кDoneшаг.

  3. Выберите самую старую микрозадачу в очереди микрозадач цикла событий.

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

  5. 执行: выполнить выбранную задачу.

Примечание: это может включать выполнениеclean up after running scriptпошаговый сценарий, который затем снова приводит кЗадачи, выполняющие контрольные точки микрозадач, что мы и собираемся использоватьperforming a microtask checkpoint flagпричина.

  1. Устанавливает для текущей выполняемой задачи цикла событий значение null.

  2. Удалить микрозадачу, выполненную на предыдущем шаге, из очереди микрозадач и вернуться处理 microtask queueшаг.

  3. 完成: для каждогоresponsible event loopтекущий цикл событийenvironment settings object,Датьenvironment settings objectотправить одинrejected promisesобъявление о.

  4. 清理 IndexedDB 的事务.

  5. Будуperforming a microtask checkpoint flagУстановите значение «ложь».

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

task & microTask

task

В основную задачу входит:

  • скрипт (общий код)
  • setTimeout
  • setInterval
  • setImmediate
  • I/O
  • UI rendering

microtask

микрозадача в основном включает в себя:

  • process.nextTick (среда Node.js)
  • Промисы (здесь имеется в виду нативный промис, реализованный браузером)
  • Object.observe (заменен на MutationObserver)
  • MutationObserver
  • postMessage

Web APIs

упоминалось в предыдущей главепользовательский агент(среда хостинга/среда выполнения/браузер) для управления планированием задач, очереди задач — это просто очередь, она не знает, когда добавляются новые задачи или когда задачи удаляются из очереди. Цикл событий будет постоянно удалять задачи из очереди в соответствии с правилами, так кто же будет ставить задачи в очередь? Ответ — веб-API.

Все мы знаем, что выполнение JavaScript является однопоточным, но браузеры не являются однопоточными.Веб-API — это дополнительные потоки, которые обычно реализуются в C++ для обработки асинхронных событий, таких как события DOM, HTTP-запросы, setTimeout и т. д. Они являются точкой входа для браузеров для реализации параллелизма, а для Node.JavaScript — некоторыми API-интерфейсами C++.

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

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

обработать

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

Сначала выполните задачу, если весь первый раунд цикла событий, то общий сценарий является задачей, код, выполняемый синхронно, будет непосредственно помещен в стек вызовов (стек вызовов), например функции setTimeout, fetch, ajax или обратного вызова события. будут вызываться веб-API, а стек вызовов продолжает выполнять функцию в верхней части стека. Когда сетевой запрос получает ответ или истекает время таймера, веб-API помещают соответствующую функцию обратного вызова в соответствующие очереди задач. Цикл событий выполняется непрерывно. Как только текущая задача в цикле событий становится нулевой, она возвращается к сканированию очередей задач, чтобы увидеть, есть ли задачи, а затем удаляет самую раннюю функцию обратного вызова в очередях задач в соответствии с определенными правилами. (например, указанная выше 75% Вероятность выполнения очереди, где функция обратного вызова мыши и клавиатуры находится первой, но конкретных правил я не нашел), функция обратного вызова, которая вынесена, помещается в контекст стек выполнения и начинает выполняться синхронно.После выполнения проверяем микрозадачу в очереди микрозадач в цикле событий.,выполняем их все синхронно по правилам,и наконец завершаем повторную отрисовку UI,а затем выполняем следующую цикл событий...

68747470733a2f2f63646e2d696d616765732d312e6d656469756d2e636f6d2f6d61782f313630302f312a2d4d4d42484b795f5a7843726f7565635271767342672e706e67

заявление

Неточность setTimeout

Механизм JavaScript не работает независимо, он работает в среде хоста.

Зная приведенные выше веб-API, мы знаем, что в браузере есть веб-API таймеров для управления такими таймерами, как setTimeout и setInterval.После синхронного выполнения setTimeout браузер не вешает вашу функцию обратного вызова в очередь цикла событий. Что он делает, так это устанавливает таймер. Когда таймер истечет, браузер поместит вашу функцию обратного вызова в цикл обработки событий, чтобы в какой-то момент в будущем тик был зафиксирован и обратный вызов был выполнен.

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

let t1, t2

t1 = new Date().getTime();

// 1
setTimeout(()=>{
    let i = 0;
    while (i < 50000000) {i++}
    console.log('block finished')
}
, 300)

// 2
setTimeout(()=>{
    t2 = new Date().getTime();
    console.log(t2 - t1)
}
, 300)

В этом примере напечатанная временная метка не будет равна 300, хотя две функции setTimeout будут поставлены в очередь веб-API, когда время истечет, а затем цикл событий выберет первый обратный вызов setTimeout и начнет выполняться, но это callback Функция будет синхронно заблокирована на некоторое время, в результате чего цикл обработки событий выполнит вторую функцию обратного вызова setTimeout только после завершения ее выполнения.

Когда входить в стек вызовов

пример 1

try {
  setTimeout(() => {
    throw new Error("Error - from try statement");
  }, 0);
} catch (e) {
  console.error(e);
}

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

Подождите, пока таймер не истечет (обратите внимание, что это не обязательно следующий цикл событий, потому что минимальное время setTimeout в каждом браузере не определено, и если вы выполните его несколько раз в Chrome, вы обнаружите, что время каждый раз отличается , 0 мс ~ 2 мс), обратный вызов в setTimeout будет помещен в очередь задач. В это время текущая задача в цикле событий равна нулю, функция обратного вызова будет установлена ​​как текущая задача, и начнется синхронное выполнение. В настоящее время существует только глобальный контекст, попытка перехвата завершена, и ошибка будет выдана напрямую.

Пример 2

for (var i = 0; i < 5; i++) {
  setTimeout((function(i) {
    console.log(i);
  })(i), i * 1000);
}

Правильный ответ - выводить "0 1 2 3 4" сразу, первый параметр setTime принимает функцию или строку, где первый параметр - функция немедленного выполнения, возвращаемое значение не определено, и в процессе немедленного выполнения «0 1 2 3 4» выводится посередине, и таймер не получает никакой функции обратного вызова, поэтому он не имеет ничего общего с циклом событий.

Пример 3

new Promise(resolve => {
    resolve(1);
    Promise.resolve().then(() => console.log(2));
    console.log(4)
}).then(t => console.log(t)); // a
console.log(3);

Это вопрос в Твиттере г-на Руана.Во-первых, объект в конструкторе Promise выполняется синхронно (для тех, кто не знает Promise, вы можете сначала прочитать егоэта статья),врезатьсяresolve(1), пометьте текущее промис как разрешение, но обратите внимание, что его функция обратного вызова then не была зарегистрирована, поскольку она еще не достигла a. Продолжайте выполнять и сталкивайтесь с другим промисом, который немедленно разрешается и выполняет свою регистрацию, отправляя функцию обратного вызова второго затем в пустую очередь микрозадач. Продолжайте выводить 4, затем then at a регистрируется только сейчас, помещая функцию обратного вызова then первого промиса в очередь микрозадач. Продолжайте выполнять вывод a 3. Теперь, когда задачи в очереди задач выполнены, при достижении флага контрольной точки микрозадачи обнаруживается, что есть две микрозадачи, которые выполняются в порядке добавления.Первая выводит 2, вторая выводит 1, и, наконец, обновить пользовательский интерфейс, а затем этот раунд. Цикл событий завершен, окончательный вывод «4 3 2 1»

Vue

Я сам не использовал Vue, но немного знаю, что в обновлении DOM Vue есть пакетные обновления, и изменения данных буферизуются в том же цикле событий, то есть DOM будет изменен только один раз.

об этомГу ИнгерУ большого парня это на Чжихуотвечать:

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

В спецификации главы цикла событий четко написано, что в одном раунде цикла событий будет соблюдаться порядок задача -> микрозадача -> отрисовка пользовательского интерфейса. Код пользователя может изменять данные много раз, и более поздняя модификация этих модификаций может перезаписать предыдущую модификацию.Кроме того, работа с DOM очень дорогая и должна быть сведена к минимуму, поэтому модификация пользователя должна быть прервана, а затем DOM изменяется только один раз, поэтому необходимо использовать microTask для выполнения перед рендерингом обновления пользовательского интерфейса.Даже если есть несколько модификаций, DOM будет изменен только один раз, а затем будет отображаться.

Обновление: реализация Vue nextTick теперь удаляет подход MutationObserver (из соображений совместимости) и вместо этого использует MessageChannel.

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

Ссылаться на

Категории