Spec-Zone.ru › Web APIs

Подробно: Микрозадачи и среда выполнения JavaScript

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

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

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

Начиная с добавления таймаутов и интервалов как части API браузера (setTimeout() и setInterval()), среда JavaScript, предоставляемая веб-браузерами, постепенно развивалась, включив в себя мощные возможности, которые позволяют планировать задачи, разрабатывать многопоточные приложения и т. д. Чтобы понять, где queueMicrotask() играет роль в этом контексте, полезно понять, как среда выполнения JavaScript работает при планировании и выполнении кода.

Контексты выполнения JavaScript

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

Когда фрагмент кода JavaScript выполняется, он выполняется внутри контекста выполнения. Существует три типа кода, которые создают новый контекст выполнения:

  • Глобальный контекст — это контекст выполнения, созданный для выполнения основной части вашего кода; то есть любого кода, который существует вне функции JavaScript.
  • Каждая функция выполняется в собственном контексте выполнения. Это часто называют «локальным контекстом».
  • Использование не рекомендуемой функции eval() также создает новый контекст выполнения.

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

const outputElem = document.getElementById("output");

const userLanguages = {
  Mike: "en",
  Teresa: "es",
};

function greetUser(user) {
  function localGreeting(user) {
    let greeting;
    const language = userLanguages[user];

    switch (language) {
      case "es":
        greeting = `¡Hola, ${user}!`;
        break;
      case "en":
      default:
        greeting = `Hello, ${user}!`;
        break;
    }
    return greeting;
  }
  outputElem.innerText += `${localGreeting(user)}\n`;
}

greetUser("Mike");
greetUser("Teresa");
greetUser("Veronica");

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

  • При запуске программы создаётся глобальный контекст.

    • Когда greetUser("Mike") достигается, создается контекст для функции greetUser(); этот контекст выполнения помещается в стек контекстов выполнения.

      • Когда greetUser() вызывает localGreeting(), создаётся ещё один контекст для выполнения этой функции. Когда эта функция возвращается, контекст для localGreeting() удаляется из стека выполнения и уничтожается. Выполнение программы возобновляется с следующим контекстом в стеке, который это greetUser(); эта функция возобновляет выполнение с того места, где остановилась.
      • Функция greetUser() возвращается, и её контекст удаляется из стека и уничтожается.
    • Когда greetUser("Teresa") достигается, создаётся контекст для него и помещается в стек.

      • Когда greetUser() вызывает localGreeting(), создаётся ещё один контекст для выполнения этой функции. Когда эта функция возвращается, контекст для localGreeting() удаляется из стека выполнения и уничтожается. greetUser() продолжает выполнение с того места, где остановился.
      • Функция greetUser() возвращается, и её контекст удаляется из стека и уничтожается.
    • Когда greetUser("Veronica") достигается, создаётся контекст для него и помещается в стек.

      • Когда greetUser() вызывает localGreeting(), создаётся ещё один контекст для выполнения этой функции. Когда эта функция возвращается, контекст для localGreeting() удаляется из стека выполнения и уничтожается.
      • Функция greetUser() возвращается, и её контекст удаляется из стека и уничтожается.
  • Главная программа завершается, и её контекст выполнения удаляется из стека выполнения; так как контекстов не остаётся в стеке, выполнение программы завершается.

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

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

Запуск JavaScript

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

Здесь мы рассмотрим работу среды выполнения немного подробнее.

Циклы обработки событий

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

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

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

Существует три типа циклов обработки событий:

Цикл обработки событий окна

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

Цикл обработки событий рабочего потока

Цикл обработки событий рабочего потока — это цикл, который управляет рабочим потоком; это включает в себя все виды рабочих потоков, включая базовые веб-рабочие потоки, общие рабочие потоки и сервисные рабочие потоки. Рабочие потоки содержатся в одном или нескольких агентах, отдельных от «основного» кода; браузер может использовать один цикл обработки событий для всех рабочих потоков данного типа или может использовать несколько циклов обработки событий для их обработки.

Цикл обработки событий Worklet

Цикл обработки событий Worklet — это цикл обработки событий, используемый для управления агентами, которые выполняют код для работы модулей Worklet для данного агента. Это включает модули Worklet типа Worklet и AudioWorklet.

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

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

  • Если одно окно открыло другое окно, они, вероятно, используют один цикл обработки событий.
  • Если окно фактически является контейнером в <iframe>, оно, вероятно, разделяет цикл обработки событий с окном, которое его содержит.
  • Окна могут использовать один и тот же процесс в реализации веб-браузера с несколькими процессами.

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

Задачи и микрозадачи

Задача — это всё, что запланировано для выполнения стандартными механизмами, такими как первоначальное начало выполнения скрипта, асинхронная отправка события и т. д. Кроме использования событий, вы можете добавить задачу в очередь с помощью setTimeout() или setInterval().

Разница между очередью задач и очередью микрозадач проста, но очень важна:

  • Когда начинается новая итерация цикла обработки событий, среда выполнения выполняет следующую задачу из очереди задач. Дополнительные задачи и задачи, добавленные в очередь после начала итерации, не будут выполнены до следующей итерации.
  • Всякий раз, когда задача завершается, а стек контекстов выполнения пуст, все микрозадачи в очереди микрозадач выполняются по очереди. Разница заключается в том, что выполнение микрозадач продолжается до тех пор, пока очередь не станет пустой — даже если в промежутке планируются новые. Другими словами, микрозадачи могут добавить новые микрозадачи в очередь, и эти новые микрозадачи будут выполнены до начала следующей задачи и до конца текущей итерации цикла обработки событий.

Проблемы

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

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

Решения

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

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

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

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

См. также

  • Руководство по микрозадачам
  • Window.queueMicrotask()
  • Очередь событий
  • Асинхронный JavaScript
    • Вступление в асинхронный JavaScript
    • Грациозное асинхронное программирование с обещаниями

© 2005–2024 MDN contributors.
Licensed under the Creative Commons Attribution-ShareAlike License v2.5 or later.
https://developer.mozilla.org/en-US/docs/Web/API/HTML_DOM_API/Microtask_guide/In_depth

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API