Event Loop

Event loop в JavaScript — менеджер асинхронных вызовов

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

С английского loop переводится как «loop», что прекрасно отражает смысл — мы имеем дело с зацикленной очередью.

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

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

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


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

Справка

В JavaScript есть понятие «контекст функции». Но есть еще один термин: «контекст исполнения». Это тело функции со всеми переменными и другими функциями, которое называется «scope», от английского — «scope». Важно не путать понятия, это принципиально разные вещи.

Итого

Более подробный алгоритм цикла событий (хотя и упрощенный, чем в спецификации):

  1. Выберите и запустите самую старую задачу из очереди макро-задач (например, «сценарий»).
  2. Выполняем все микрозадания:
    • Пока очередь микрозадач не пуста: — Выберите из очереди и запустите самую старую микрозадачу
  3. Внесите изменения на страницу, если таковые имеются.
  4. Если очередь задач макроса пуста, подождите, пока не отобразится задача макроса.
  5. Переходите к шагу 1.

Чтобы добавить новую макрос-задачу в очередь:

  • Используйте setTimeout (f) с нулевой задержкой.

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

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

Чтобы добавить новую микрозадачу в очередь:

  • Используйте queueMicrotask (f).
  • Кроме того, обработчики обещаний выполняются внутри очереди микрозадач.

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

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

Веб-воркеры

Для долгих и тяжелых вычислений, которые не должны блокировать цикл событий, мы можем использовать Web Workers.

Это способ запустить код в другом параллельном потоке.

Веб-работники могут обмениваться сообщениями с основным процессом, но у них есть свои собственные переменные и свой собственный цикл событий.

Веб-воркеры не имеют доступа к DOM, поэтому их основное использование — вычисления. Они позволяют использовать несколько ядер процессора одновременно.

Use case 3: doing something after the event

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

В главе «Отправка настраиваемых событий» мы видели пример: настраиваемое событие открытия меню отправляется в setTimeout, поэтому оно происходит после того, как событие «щелчок» было полностью обработано.

menu.onclick = function () {//… // создаем настраиваемое событие с данными выбранного пункта меню let customEvent = new CustomEvent («menu-open», {пузыри: true}); // асинхронно отправляем настраиваемое событие setTimeout (() => menu.dispatchEvent (customEvent)); };

Introduction

Цикл событий — один из наиболее важных аспектов JavaScript.

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

Этот пост призван объяснить внутренние детали того, как JavaScript работает с одним потоком и как он обрабатывает асинхронные функции.

Ваш код JavaScript работает в однопоточном режиме. В каждый момент времени может быть только одно.

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

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

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

Среда обрабатывает несколько одновременных циклов событий, например, для обработки вызовов API. Веб-воркеры также работают в собственном цикле событий.

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

A simple event loop explanation

Возьмем пример:

Я использую foo, bar и baz как случайные имена. Введите любое имя, чтобы заменить их

constbar = () => console.log (‘bar’) constbaz = () => console.log (‘baz’) constfoo = () => {console.log (‘foo’) bar () baz ()} Фу()

Этот код напечатан

foo bar baz

как и ожидалось.

Когда этот код выполняется, сначала вызывается foo (). Внутри foo () мы сначала вызываем bar (), затем вызываем baz().

На данный момент стек вызовов выглядит так:

Цикл событий на каждой итерации ищет что-то в стеке вызовов и выполняет это:

пока стек вызовов не станет пустым.

Как формируется контекст исполнения

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

Как только скрипт попадает в интерпретатор, формируются глобальный контекст и глобальная область видимости, в которой содержится переменный объект, или VO — переменный объект.

он состоит из переменных типа объявления функции и атрибутов функции по следующему принципу. Интерпретатор читает код и находит все объявления:

  • переменные, которые используют ключевое слово var (const или let в ES6 и новее);
  • функции, объявленные с неназначенным ключевым словом function.

Это дополнение к VO текущего контекста выполнения. Затем берется переменный объект внешнего осциллографа и к нему добавляется образованная выше ВО. Сверху он интегрирован с параметрами функции и их значениями во время выполнения.

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

Рассмотрим сценарий:

переменная a = 10; var c = 7; functionfunc (a, b, d) {console.log (a, b, c, d); с = а + d; } переменная b = 3; функция (10, а, б); console.log (c);

VO этого скрипта генерируется:

  1. Из переменной a, значение которой не определено.
  2. Переменная c, значение которой не определено.
  3. Переменная b, значение которой не определено.
  4. Func с соответствующим телом.

Затем скрипт запустится по следующему сценарию:

  1. Значение 10 будет записано в переменную a.
  2. Значение 7 будет записано в переменную c.
  3. В переменную b будет записано значение 3.
  4. Будет вызвана функция func.
  5. Создается контекст выполнения функции func.
  6. В VO контекста выполнения функции func будут записаны переменные из внешней области: a, c и b с присвоенными значениями.
  7. Контекст выполнения функции VO создаст переменные из списка аргументов; поскольку переменные a и b уже существуют в VO, будет добавлена ​​только переменная d с неопределенным значением.
  8. Значение 10 будет записано в переменную a VO контекста выполнения функции func.
  9. Переменной b VO контекста выполнения функции func будет присвоено значение переменной a внешней области видимости — 10.
  10. Переменная d VO контекста выполнения функции func будет установлена ​​в значение переменной b внешней области видимости — 3.
  11. Будет запущен контекст выполнения функции func.
  12. На консоли отобразится 1010 7 3.
  13. Переменная c во внешней области видимости будет установлена ​​на 13.
  14. Контекст выполнения функции func будет завершен; Функция VO func будет удалена.
  15. На консоли отобразится 13.

Теперь давайте перепишем скрипт, добавив setTimeout с нулевым таймаутом к вызову функции:

переменная a = 10; var c = 7; functionfunc (a, b, d) {console.log (a, b, c, d); с = а + d; } переменная b = 3; setTimeout (функция () {функция (10, а, б);}, 0); console.log (c);

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

  1. Значение 10 будет записано в переменную a.
  2. Значение 7 будет записано в переменную c.
  3. В переменную b будет записано значение 3.
  4. Функция func попадает в пул ожидания.
  5. Создается контекст выполнения функции func.
  6. Через 0 миллисекунд контекст выполнения функции func будет вставлен в цикл событий.
  7. На консоли отобразится 7.
  8. В VO контекста выполнения функции func будут записаны переменные из внешней области: a, c и b с присвоенными значениями.
  9. Контекст выполнения функции VO создаст переменные из списка аргументов; поскольку переменные a и b уже существуют в VO, будет добавлена ​​только переменная d с неопределенным значением.
  10. Значение 10 будет записано в переменную a VO контекста выполнения функции func.
  11. Переменной b VO контекста выполнения функции func будет присвоено значение переменной a внешней области видимости — 10.
  12. Переменная d VO контекста выполнения функции func будет установлена ​​в значение переменной b внешней области видимости — 3.
  13. Будет запущен контекст выполнения функции func.
  14. На консоли отобразится 1010 7 3.
  15. Переменная c во внешней области видимости будет установлена ​​на 13.
  16. Контекст выполнения функции func будет завершен; Функция VO func будет удалена.

How JavaScript works in the browser

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

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

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

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

Call stack

Вы, наверное, уже слышали, что JavaScript является однопоточным. Но что это значит?

JavaScript может делать что-то одно, потому что у него только один стек вызовов.

Стек вызовов — это механизм, который помогает интерпретатору JavaScript отслеживать функции, вызываемые сценарием.

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

Функция завершается с помощью оператора возврата или по достижении конца области видимости.

Я создал эту небольшую визуализацию, чтобы облегчить понимание:

const addOne = (значение) => значение +1;
const addTwo = (значение) => addOne (значение +1);
const addTre = (значение) => addTwo (значение +1);
расчет const = () => {
returnaddTre (1) + addDue (2);<br>};
расчет();
addOne (3) addTwo (2) addTre (1) вычисление () main()

Обратите внимание, как каждый вызов функции добавляется в стек вызовов и удаляется по завершении.

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

Порядок, в котором стек обрабатывает каждый вызов функции, соответствует принципу LIFO (Last In, First Out).

Шаги в предыдущем примере следующие:

  1. Файл загружается и вызывается функция main, которая указывает выполнение всего файла. Эта функция добавляется в стек вызовов.
  2. вычисление основных вызовов (), поэтому оно добавляется в начало стека вызовов.
  3. computation () вызывает addThree (), который добавляется обратно в стек вызовов.
  4. addThree вызывает addTwo, который добавляется в стек вызовов.
  1. addOne не вызывает никаких других функций. Когда он выходит, он удаляется из стека вызовов.
  2. В результате addOne addTwo также завершает работу и удаляется из стека вызовов.
  3. addThree также удаляется.
  4. вычисление вызывает addTwo, который добавляет его в стек вызовов.
  5. addTwo вызывает addOne и добавляет его в стек вызовов.
  6. addOne завершает работу и удаляется из стека вызовов.
  7. addTwo завершает работу и удаляется из стека вызовов.
  8. теперь вычисление может завершиться с результатом addThree и addTwo и будет удалено из стека вызовов.
  9. В файле нет дополнительных операторов или вызовов функций, поэтому main также завершает работу и удаляется из стека вызовов.

Я вызвал контекст, в котором выполняется наш основной код, но это не официальное название функции. В сообщениях об ошибках, которые вы можете найти в консоли браузера, эта функция называется анонимной.

Uncaught RangeError: Maximum call stack size exceeded

Вы, вероятно, знаете стек вызовов по отладке кода. Uncaught RangeError: превышение максимального размера стека вызовов — одна из возможных ошибок. Ниже мы можем увидеть снимок стека вызовов, когда произошла ошибка.

Следите за трассировкой стека для этого сообщения об ошибке. Представляет вызовы функций, которые привели к этой ошибке. В этом случае ошибка была в функции b, которая была вызвана a (которая была вызвана b и т.д.).

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

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

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

functiona () {b ();} functionb () {a ();} a();

Таким образом, стек вызовов отслеживает вызовы функций в коде. Он следует принципу LIFO (Last In, First Out), что означает, что он всегда сначала обрабатывает вызов, который находится на вершине стека.

У JavaScript есть только один стек вызовов, поэтому он может делать только одну вещь за раз.

Heap

Куча JavaScript — это место, где хранятся объекты, когда мы определяем функции или переменные.

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

 

Web APIs

Выше я упоминал, что JavaScript может делать только одну вещь за раз.

Хотя это верно в отношении самого языка JavaScript, вы все равно можете делать что-то одновременно в браузере. Как уже следует из названия, это возможно через API, предоставляемые браузерами.

Например, давайте посмотрим, как мы делаем запрос к API. Если бы мы запустили код внутри интерпретатора JavaScript, мы не смогли бы делать что-либо еще, пока не получим ответ от сервера.

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

В качестве решения этой проблемы веб-браузеры предоставляют нам API-интерфейсы, которые мы можем вызывать в нашем коде JavaScript. Однако выполнение обрабатывается самой платформой, поэтому она не блокирует стек вызовов.

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

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

 

Callback queue

Благодаря возможностям веб-API теперь мы можем выполнять операции одновременно вне интерпретатора JavaScript. Но что, если мы хотим, чтобы наш код JavaScript реагировал на результат веб-API, такой как запрос AJAX?

здесь в игру вступают обратные вызовы. Через них веб-API позволяют нам выполнять код после завершения вызова API.

What is a callback?

Обратный вызов — это функция, переданная в качестве аргумента другой функции. Обратный вызов обычно выполняется после завершения кода.

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

Давайте посмотрим на пример:

состоит = () => console.log (‘а’); constb = () => setTimeout (() => console.log (‘b’), 100); constc = () => console.log (‘c’); к (); б (); c();

setTimeout добавляет тайм-аут в x мс перед выполнением обратного вызова.

Вы, наверное, уже думаете, как будет выглядеть результат.

setTimeout выполняется одновременно, в то время как интерпретатор JS продолжает выполнять последующие операторы.

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

Окончательный результат будет выглядеть так:

акб

Event loop

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

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

Вот почему важно не блокировать стек вызовов при выполнении ресурсоемких задач.

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

Обработчики событий, такие как onscroll, добавляют больше задач в очередь обратного вызова при запуске. Вот почему вы должны избавиться от этих обратных вызовов, что означает, что они будут запускаться только каждые x мс.

See it for yourself

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

window.onscroll = () => console.log (‘прокрутка’);

setTimeout(fn, 0)

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

Размещение асинхронного кода в обратном вызове и установка для setTimeout значения 0 мс позволит браузеру выполнять такие операции, как обновление DOM, прежде чем продолжить выполнение обратного вызова.

Job queue and asynchronous code

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

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

Promises: A quick recap

EcmaScript 2015 (или ES6) впервые представил обещания, хотя он уже был доступен в Babel.

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

setTimeout (() => {console.log (‘Распечатайте это и подождите’); setTimeout (() => {console.log (‘Сделайте что-нибудь еще и подождите’); setTimeout (() => {//.. }, 100);}, 100);}, 100)

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

С обещаниями этот код может стать более читаемым:

// Обертка обещания для setTimeoutconsttimeout = (time) => newPromise (resolve => setTimeout (resolve, time)); timeout (1000) .then (() => {console.log (‘Привет через 1 секунду’); returntimeout (1000);}) then (() => {console.log (‘Привет через 2 секунды’);});

Этот код выглядит еще более читаемым с синтаксисом async / wait:

constlogDelayedMessages = async () => {awaittimeout (1000); console.log («Привет через 1 секунду»); время ожидания (1000); console.log («Привет через 2 секунды»);}; logDelayedMessages();

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

Where do promises fit in?

Почему я говорю об обещаниях?

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

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

Цикл событий будет принимать вызовы из очереди обещаний перед обработкой очереди обратного вызова.

Давайте посмотрим на пример:

console.log (‘а’); setTimeout (() => console.log (‘b’), 0); newPromise ((решить, отклонить) => {решить ();}) then (() => {console.log (‘c’);}); console.log (‘д’);

Принимая во внимание ваши знания о том, как работают очереди обратного вызова, вы можете подумать, что на выходе будет adb c.

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

adcb

Пример 1: разбиение «тяжёлой» задачи.

Допустим, у нас есть бизнес, требующий значительных ресурсов процессора.

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

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

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

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

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

позвольте мне = 0; пусть start = Date.now (); function count () {// выполняет тяжелую работу для (let j = 0; j <1e9; j ++) {i ++; } alert («Сделано между» + (Date.now () — start) + ‘ms’); } считать();

Браузер также может отображать сообщение «сценарий занимает слишком много времени».

Давайте проанализируем активность с помощью вложенного setTimeout:

позвольте мне = 0; пусть start = Date.now (); function count () {// это часть тяжелой работы (*) do {i ++; } while (i% 1e6! = 0); if (i == 1e9) {уведомление («Сделано через» + (Date.time () — начало) + ‘мс’); } еще {setTimeout (счетчик); // планируем новый вызов (**)}} count();

Теперь интерфейс браузера полностью работоспособен во время «подсчета».

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

  1. Считается первый запуск: i = 1… 1000000.
  2. Считается второе выполнение: i = 1000001… 2000000.
  3. …так далее.

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

Обратите внимание, что оба варианта, с разделением задачи setTimeout и без него, сопоставимы по скорости выполнения. В общем времени счета нет большой разницы.

Чтобы еще больше сузить разницу, давайте немного улучшим наш код.

Переместим расписание следующего вызова в начало count():

позвольте мне = 0; пусть start = Date.now (); function count () {// перемещаем расписание следующего звонка в начало if (i <1e9 — 1e6) {setTimeout (count); // планируем новый вызов} do {i ++; } while (i% 1e6! = 0); if (i == 1e9) {уведомление («Сделано через» + (Date.time () — начало) + ‘мс’); }} считать();

Теперь, когда мы запускаем count () и видим, что нам нужно снова запустить count (), мы планируем этот вызов прямо перед запуском задания.

Если вы запустите этот код, вы легко заметите, что он занимает намного меньше времени.

Почему?

это просто: помните, что минимальная задержка браузера составляет 4 миллисекунды с множеством вложенных вызовов setTimeout. Даже если мы укажем задержку равной 0, на самом деле это будет 4 мс (или немного больше). Следовательно, чем раньше вы запланируете его запуск, тем быстрее будет работать ваш код.

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

Execution Workflow

События, таймеры, запросы Ajax предоставляются браузерами на стороне клиента и часто называются веб-API. Именно они позволяют однопоточному JavaScript быть неблокирующим, параллельным и асинхронным! Но как?

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

The Call Stack

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

Когда вызывается функция printSquare (), она помещается в стек вызовов, функция printSquare () вызывает функцию square (). Функция square () помещается в стек, а также вызывает функцию multiply (). Функция умножения помещается в стек. Поскольку функция умножения возвращается и помещается в стек в последнюю очередь, она сначала разрешается и удаляется из стека, затем следует функция square (), а затем функция printSquare.

The Web API

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

Вернемся к нашему примеру с setTimeout выше;

Когда мы запускаем код, первая строка console.log помещается в стек, и мы получаем наш вывод почти сразу, когда доходит до тайм-аута, таймеры обрабатываются браузером и не являются частью основной реализации V8, это вместо этого отправляется в API Web, освобождая стек, чтобы он мог выполнять другие операции.

Пока тайм-аут все еще работает, стек переходит к следующей строке действия и выполняет последний console.log, что объясняет, почему мы получаем его до вывода таймера. Как только таймер сработает, что-то произойдет. Console.log на таймере волшебным образом снова появляется в стеке вызовов!

The Event Loop

Прежде чем обсуждать цикл событий, давайте сначала рассмотрим функцию очереди активности.

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

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

Но ждать. ЧТО ТАКОЕ ЦИКЛ СОБЫТИЙ???

Цикл событий — это процесс, который ожидает очистки стека вызовов перед отправкой обратных вызовов из очереди действий в стек вызовов. После очистки стека цикл событий запускается и проверяет очередь активности на наличие доступных обратных вызовов. Если они есть, он отправляет их в стек вызовов, ожидает, пока стек вызовов снова не освободится, и повторяет тот же процесс.

Заключение

Хотя это очень простое введение, концепция асинхронного программирования в JavaScript предлагает достаточно информации, чтобы четко понять, что происходит под капотом и как JavaScript может работать одновременно и асинхронно только с одним потоком.

JavaScript всегда по запросу, и если вам интересно научиться, я рекомендую пройти этот курс Udemy.

 

Как события добавляются в очередь

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

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

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

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

Посмотрим, как отложить выполнение функции до очистки стека.

Пример использования setTimeout (() => {}), 0) — вызвать функцию, но выполнить ее после того, как все другие функции в коде были выполнены.

Пример:

const bar = () => console.log (‘bar’) const baz = () => console.log (‘baz’) const foo = () => {console.log (‘foo’) setTimeout (bar, 0) baz ()} foo()

bar, baz, foo — случайные имена.

Когда код выполняется, сначала вызывается foo (). Внутри foo () мы сначала вызываем setTimeout Passing bar в качестве аргумента и приказываем ему запускаться как можно быстрее, передавая 0 в качестве таймера. Поэтому мы вызываем baz().

Порядок функций в программе:

цикл событий js 2

Как работает асинхронный JavaScript?

Теперь, когда у вас есть представление о стеке вызовов и синхронном JS, вы можете перейти к асинхронному JavaScript.

Что такое Блокировка?

Предположим, мы обрабатываем изображение и сетевой запрос синхронно. Например:

const processImage = (image) => {/ ** * путем выполнения некоторых операций с изображением ** / console.log («Обработанное изображение»); } const networkRequest = (url) => {/ ** * запрос сетевого ресурса ** / return someData; } константное приветствие = () => {console.log (‘Привет, мир’); } processImage (logo.jpg); networkRequest (‘www.somerandomurl.com’); приветствие();

Обработка образа и сетевого запроса займет некоторое время. Продолжительность функции processImage () зависит от размера изображения.

После его завершения функция processImage () извлекается из стека, после чего мы видим вызов функции networkRequest (), которая помещается в стек. И все это требует времени.

После завершения networkRequest () вызывается функция welcome (), которая выполняется немедленно, так как содержит только console.log, что довольно быстро.

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

Какое здесь может быть решение?

Самое простое решение — использовать асинхронные обратные вызовы, чтобы сохранить наш код из блока. Например:

const networkRequest = () => {setTimeout (() => {console.log (‘Асинхронный код’);}, 2000); }; console.log («Привет, мир»); networkRequest();

Я использовал метод setTimeout для имитации сетевого запроса. Обратите внимание, что этот метод является частью веб-API (в браузере) и C / C ++ API (в node.js), движок JavaScript не имеет этого метода.

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

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

Посмотрим, как код работает асинхронно.

const networkRequest = () => {setTimeout (() => {console.log (‘Асинхронный код’);}, 2000); }; console.log («Привет, мир»); networkRequest (); console.log («Конец’);

Когда этот код загружается в браузер, console.log («Hello World») помещается в стек и удаляется из стека по завершении. Затем networkRequest () помещается в стек().

Следующей в стеке идет функция setTimeout (). Эта функция имеет два аргумента: 1) обратный вызов и 2) время в микросекундах (мкс).

Метод setTimeout () запускает таймер на 2 секунды в среде веб-API.На этом этапе выполнение setTimeout () завершается и снимается со стека. Затем console.log («Конец») помещается в стек и после его завершения также удаляется из стека.

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

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

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

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

Затем console.log («Асинхронный код») помещается в стек и после выполнения удаляется из него. На этом обратный вызов завершен и снимается со стека. Программа наконец-то завершена.

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

document.querySelector (‘btn’) addEventListener (‘щелчок’, (событие) => {console.log (‘Нажата кнопка’); });

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

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

Отсрочка Выполнения Функции

Мы также можем использовать setTimeout, чтобы отложить выполнение функции до тех пор, пока стек не будет очищен. Пример:

const bar = () => {console.log (‘бар’); } const baz = () => {console.log (‘baz’); } const foo = () => {console.log (‘foo’); setTimeout (бар, 0); baz (); } Foo();

Результат исполнения:

foo baz bar

Давайте посмотрим на этот код: сначала вызывается функция foo (); внутри foo называется console.log (‘foo’); поэтому setTimeout () вызывается с bar () в качестве обратного вызова и таймером 0 секунд.

Если мы не используем setTimeout, функция bar () будет выполнена немедленно, но используя setTimeout с таймером 0 секунд, мы откладываем выполнение бара до тех пор, пока стек не опустеет.

По истечении таймера функция обратного вызова bar () помещается в очередь сообщений, где ожидает выполнения. Но он начнет выполняться только тогда, когда стек будет полностью пуст, то есть после завершения функций baz и foo.

 

Оцените статью
Блог о JavaScript