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

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

Но

«JavaScript — это синхронный однопоточный язык»

Что это значит, разве JavaScript не асинхронный? и ответ на этот вопрос большой жирный ДА это не так, у движка javaScript нет врожденного чувства времени, а вместо этого это просто среда выполнения по запросу для любого произвольного фрагмента javaScript .

Асинхронность/многопоточность — это всего лишь иллюзия, предоставляемая JS с помощью нескольких структур данных, и для совместной организации этих различных структур данных среда JS использует процесс, который мы называем Event loop. Сам движок JavaScript никогда не делал ничего, кроме выполнения одного фрагмента вашей программы в заданный момент по запросу?.

Подожди секунду, спросил кто? 🤔….. спросила окружающая среда. Движок JS не работает изолированно, он работает внутри среды хостинга, которая для большинства разработчиков является просто браузером или node.js, а общим знаменателем во всех этих средах является встроенный механизм, называемый циклом событий, который обрабатывает выполнение. нескольких фрагментов вашей программы с течением времени, каждый раз вызывая Js-движок.

Это окружающая среда, которая планирует события для выполнения js-движка.

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

например :

function printMessage(message) {
   let n = 10000000000;
   while (n > 0) n--;
   console.log(message);
}
console.log("start execution");
printMessage("calling an api");
console.log("end execution");

Здесь блокирующей функцией является printMessage(), скрипт зависает на несколько секунд и выводит следующий вывод:

start execution
calling an api <--- after some delay
enc execution

Веб-API для спасения

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

console.log("start execution")
setTimeout(() => {
  printMessage("calling an API")
}, 1000)
console.log("end execution")

здесь вы увидите «начать выполнение» и «завершить выполнение» сразу, но после этого «вызов API».

setTimeout(), запрос на выборку и события DOM являются частями веб-API веб-браузера, вы можете думать о нем как о помощнике для механизма javaScript, предоставляемого средой (веб-браузером). В приведенном выше примере мы использовали setTimeout(), движок JavaScript помещает его в стек вызовов, а веб-API создает таймер, который истекает через 1 секунду. Затем движок JavaScript помещает printMessage() функцию в очередь, называемую очередью обратного вызова или очередью задач или очередью сообщений.

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

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

Некоторые ключевые моменты, о которых следует помнить в отношении фрагмента кода в нашем коде Js:

  • У нас есть два типа задач/фрагментов в JavaScript, это микрозадачи и макрозадачи с разными приоритетами, когда дело доходит до выполнения.
  • Макрозадачи называются Заданиями, а микрозадачи называются Заданиями.
  • Очередь в модели цикла событий содержит вызываемые задачи (или макрозадачи), Очередь задач/ Очередь сообщений/Очередь обратного вызова, а очередь в модели цикла событий содержит вызываемые задания (или микрозадачи), JobQueue или MicroTask Queue.
  • Примерами макрозадач являются setTimeout, setInterval, setImmediate, задачи ввода-вывода и т. д. примерами микрозадач являются Promises,processes.nextTick и т. д.

Цикл событий выполняет свою работу итерациями или «тиками». Код JavaScript выполняется в режиме выполнения до завершения (текущая задача всегда завершается до выполнения следующей задачи), поэтому каждый раз, когда задача завершается, цикл событий проверяет, возвращает ли он управление другому коду. Если это не так, он запускает все задачи в очереди заданий, а затем запускает задачи в очереди задач.

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

1. console.log(1);
2. setTimeout(() => console.log(2));
3. Promise.resolve().then(() => console.log(3));
4. Promise.resolve().then(() => setTimeout(() => console.log(4)));
5. Promise.resolve().then(() => console.log(5));
6. setTimeout(() => console.log(6));
7. console.log(7);

Здесь

Глобальный контекст выполнения будет создан как контекст выполнения по умолчанию/базовый и будет помещен в конец нашего стека вызовов.

Для строки номер 1 нашего кода будет создан контекст выполнения для console.log(1), и он будет выполнен сразу же, так как не использует очередь, а также удален из стека вызовов. И очередь микрозадач (очередь сообщений/очередь задач), и очередь макрозадач (очередь заданий) на данный момент пусты.

Состояние выхода: 1

Строка номер 2 — это setTimeout, который представляет собой веб-API, поэтому он не отображается в стеке вызовов, но возвращаемый им обратный вызов () => console.log(2) помещается в очередь задач, готовый к вызову в будущем повторение цикла событий.

Состояние вывода: 1, состояние очереди задач: () => console.log(2), очередь микрозадач: empty

Строка номер 3, используя метод then для промиса, мы указываем функцию, которая должна запускаться после ее установки. Урегулированное обещание — это обещание, которое переместилось из pending (когда он выполняет базовый процесс, такой как выборка данных) либо в fulfilled (успешно), либо в rejected (ошибка). Когда он устанавливается, он ставит в очередь микрозадачу для своего обратного вызова, поэтому здесь обратный вызов добавляется в очередь микрозадач.

Состояние вывода: 1, состояние очереди задач: () => console.log(2), очередь микрозадач: () => console.log(3)

В строке номер 4 метод then возвращает обратный вызов, который имеет значение setTimeout и будет добавлен в очередь микрозадач.

Состояние вывода: 1, состояние очереди задач: () => console.log(2), очередь микрозадач: () => console.log(3), () => setTimeout(()=> console.log(4))

В строке номер 5 функция обратного вызова () => console.log(5) будет поставлена ​​в очередь в очередь микрозадач, так как здесь мы имеем дело с промисами.

Состояние вывода: 1, состояние очереди задач: () => console.log(2) , очередь микрозадач: () => console.log(3), () => setTimeout(()=> console.log(4)),() => console.log(5)

Строка номер 6, setTimeout добавляет callback() => console.log(6) в очередь задач.

Состояние вывода: 1, состояние очереди задач: () => console.log(2),() => console.log(6) , очередь микрозадач: () => console.log(3), () => setTimeout(()=> console.log(4)),() => console.log(5)

Строка номер 7 будет выполнена сразу

Состояние вывода: 1 7, состояние очереди задач: () => console.log(2),() => console.log(6) , очередь микрозадач: () => console.log(3), () => setTimeout(()=> console.log(4)),() => console.log(5)

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

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

  • Очередь микрозадач имеет обратные вызовы: () => console.log(3), () => setTimeout(()=> console.log(4)),() => console.log(5).
  • Мы получим выходные данные 3 и 5, но setTimeout добавит обратный вызов () => console.log(4) в конец очереди задач (очереди макрозадач).
  • В очереди макрозадач теперь () => console.log(2),() => console.log(6), () => console.log(4)
  • Наше состояние вывода теперь 1 7 3 5

Когда очередь микрозадач становится пустой, выполняется очередь макрозадач. Выводит 2, 6, 4. Таким образом, состояние вывода теперь будет 1 7 3 5 2 6 4.

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

Заключение

Мы узнали, что javascript на самом деле не является асинхронным и скорее дает нам иллюзию, используя несколько интеллектуальных структур данных и постоянно работающий процесс, называемый циклом событий, чтобы должным образом сделать поток асинхронным. Если вы хотите углубиться, весь цикл событий V8/Chrome содержится в небольшой библиотеке под названием libuv, также есть полезный API, предоставляемый браузерами, который можно использовать для создания синхронного кода. асинхронный с использованием очереди микрозадач, это queueMicrotask().

Мир из ❤.