Цикл событий как выполняется асинхронный JavaScript-код в
Вы наверняка слышали о знаменитом цикле событий Node.js и о том, как ему удается обеспечивать невероятную скорость без блокировок, располагая всего лишь одним потоком выполнения кода. Ввод-вывод в Node.js событийно управляемый, и все действия выполняются в форме коллбэков – функций обратного вызова. Для организации приложения важно понимать, в каком порядке цикл событий эти коллбэки запускает. Давайте разбираться.
Фазы цикла событий
Цикл событий состоит из нескольких фаз, которые повторяются на каждой итерации одна за другой. В этой статье мы заглянем одним глазом на нижние уровни архитектуры Node.js и посмотрим, что это за фазы и какой код они выполняют.
Схематическое изображение цикла событий в Node.js
Существует ошибочное представление, что в Node есть только одна глобальная очередь коллбэков. На самом деле, каждая фаза имеет собственную очередь/кучу.
Общая схема работы выглядит так: попадая на определенную фазу, цикл событий выполняет некоторые специфические действия, затем выполняет все коллбэки из очереди этой фазы (до определенного предела) и переходит дальше.
Таймеры
Функции обратного вызова таймеров ( setTimeout , setInterval ) хранятся в куче до того момента, пока не истечет их срок действия. Если в очереди есть несколько таких «просроченных» коллбэков, цикл событий начинает вызывать их в порядке возрастания задержки, пока они не кончатся.
Выполнение таймеров контролируется в фазе опроса, до которой мы очень скоро доберемся. Если цикл долго задерживается в poll-фазе (блокировка), выполнение функций таймеров может быть задержано.
I/O коллбэки
На этом этапе цикл событий выполняет обратные вызовы системных операций ввода-вывода, которые были отложены на предыдущей итерации.
Например, вы пишете Node-сервер. Порт, на котором вы хотите запустить процесс, уже используется другим процессом. Node выдаст ошибку ECONNREFUSED . Некоторые *nix-системы могут ожидать получения сообщения об ошибке. Такие вызовы помещаются в очередь этой фазы цикла событий.
Ожидание/Подготовка
На этой фазе для нас не происходит ничего интересного.
Опрос
Цикл событий проверяет, появились ли в очереди новые асинхронные коллбэки. Здесь выполняются почти все наши функции обратного вызова кроме setTimeout , setInterval , setImmediate и close -функций.
Если в начале этой фазы в очереди уже есть обратные вызовы, все они будут выполнены по порядку (за раз выполняется не более определенного числа коллбэков).
Если очередь пуста, возможно несколько вариантов:
- Если в очереди setImmediate что-то есть, то цикл событий сразу завершит фазу опроса и перейдет к фазе проверки. Здесь он последовательно выполнит все setImmediate -коллбэки.
- Если очередь setImmediate пуста, то будет проверена очередь таймеров. Если какой-то таймер уже завершился, то цикл перейдет в первую фазу (timers) для выполнения функций обратного вызова.
- Если же нет ни событий setImmediate , ни готовых таймеров, цикл событий останется в poll-состоянии и будет ожидать добавления в очередь новых коллбэков.
Проверка/setImmediate
К этой фазе цикл событий приходит, когда в фазе опроса не осталось никаких обратных вызовов. Здесь выполняются коллбэки функции setImmediate .
Close-коллбэки
Последними в цикле выполняются функции обратного вызова, связанные с внезапными close -событиями, например, socket.on(‘close’, fn) или process.exit() .
Микротаски
Помимо всего этого, в Node есть еще одна очередь микрозадач. В нее помещаются коллбэки метода process.nextTick() , имеющие максимальный приоритет вне зависимости от фазы цикла событий.
Примеры
setTimeout vs setImmediate
Начнем с простых таймеров (на самом деле, они только кажутся простыми):
Ожидаемый вывод этого фрагмента:
Цикл событий начинается с фазы таймеров, выполняет обратный вызов для setTimeout , переходит к следующим фазам, в которых нет никаких коллбэков, достигает фазы проверки и выполняет функцию обратного вызова для setImmediate .
Вы можете справедливо возмутиться, что вызов setTimeout с задержкой в 0 секунд не выполняется сразу же. А значит первый коллбэк не выполнится в фазе таймеров.
Может быть, наш код будет выводить такой результат?
Если вы запустите этот фрагмент в Node несколько раз, то увидите, что возможны оба варианта.
Важно понимать, что когда цикл событий встречает обратный вызов, он сначала просто регистрирует его в соответствующей очереди. Только после этого начинается выполнение по фазам.
Существует неправильное представление, что setTimeout(fn, 0) всегда выполняется до setImmediate . Как мы только что видели, это далеко не всегда так. setTimeout всегда имеет небольшую задержку (4-20мс). Если она успеет истечь до наступления фазы таймеров (после регистрации коллбэков), то вызов будет выполнен. Иначе сначала вызовется функция, связанная с setImmediate . Это поведение невозможно предсказать – оно зависит от количества коллбэков, фазы цикла и пр.
I/O-коллбэки и сдвиг цикла
Однако если мы немного перепишем код:
Вывод всегда будет следующим:
Что здесь происходит?
- Когда мы вызываем функцию main(), цикл событий изначально выполняется без фактического вызова коллбэков. Он видит функцию fs.readFile и регистрирует ее обратный вызов в очереди I/O callbacks. После этого цикл переходит к реальному выполнению кода.
- Он начинает с фазы таймеров и ничего там не находит.
- В фазе I/O коллбэков тоже нет выполненных вызовов.
- Когда операция чтения файла будет завершена, цикл событий выполнит ее коллбэк (в I/O фазе).
- Затем он перейдет в фазу проверки ( setImmediate ) и лишь затем – на новую итерацию, в фазу таймеров. Таким образом в I/O-коллбэках setImmediate всегда выполняется раньше, чем setTimeout(fn, 0) .
Микротаски
Итак, как работает этот код:
- Регистрирует коллбэки по соответствующим очередям.
- Выполняет два микротаска.
- Переходит в фазу таймеров, но вызов setTimeout с задержкой в 50 мс еще не готов.
- Выполняет коллбэк setImmediate в фазе проверки.
- Обратный вызов setTimeout выполняется уже на следующей итерации.
Асинхронные микротаски
А что произойдет, если в метод process. nextTick передать асинхронную функцию?
Результат будет таким:
Разберемся, откуда что взялось:
- Коллбэки регистрируются в соответствующих очередях.
- Выполняется первый синхронный микротаск – в консоль выводится 2 .
- Начинает выполняться второй микротаск. Коллбэк setTimeout из него помещается в очередь фазы таймеров.
- После этого цикл событий начинает работать в обычном режиме, начиная с фазы таймеров.
- Время первого setTimeout (50 мс) еще не истекло, двигаемся дальше.
- Выполняется коллбэк setImmediate в фазе проверки – в консоль выводится 3 .
- Начинается новая итерация, цикл возвращается в фазу таймеров. Тут остались две функции обратного вызова setTimeout, которые и выполнятся по очереди, выведя в консоль 1 и 4.
Все вместе
Разобравшись с фазами цикла событий и очередью микротасков, мы можем испытать свои знания на новом комплексном примере:
На гифке представлена схема работы цикла событий для этого примера:
Номера очередей на картинке – это номера строк обратных вызовов в коде
Этот фрагмент кода выведет следущий результат:
Или вот такой – вспоминаем первый пример:
Определения
Микрозадачи
В Node. js (а точнее в движке V8) существует понятие микрозадач. Они являются именно частью движка, а не частью цикла событий. Кроме process.nextTick() к ним относится, например, метод Promise.resolve() .
Микрозадачи имеют приоритет перед всеми другими задачами. Как только что-то попадает в очередь микрозадач – оно сразу же выполняется (после завершения текущей операции).
Но если вы поместите много коллбэков в эту очередь, то можете вызвать “голодание” цикла событий.
Макрозадачи
Такие задачи, как setTimeout , setInterval , setImmediate , requestAnimationFrame , I/O , рендеринг пользовательского интерфейса или другие функции обратного вызова относятся к макрозадачам. У них нет никаких приоритетов, а выполнение определяется фазой цикла событий.
Итерация (tick) цикла событий
Один тик цикла соответствует проходу по всем фазам и возвращение к началу. Он характеризуется частотой (количество итераций в единицу времени) и длительностью (время затраченное на одну итерацию).
Event Loop, Callbacks, Promises, и Async/Await, часть 1 из 4
На заре Интернета веб-сайты часто состояли из статичных HTML-страниц. Но теперь, когда веб-приложения стали интерактивными и динамичными, очень часто приходится выполнять интенсивные операции, например, выполнение внешних сетевых запросов для получения данных из API. Для обработки этих операций в JavaScript разработчик должен использовать техники асинхронного программирования.
Поскольку JavaScript — однопоточный язык программирования с моделью синхронного выполнения, которая обрабатывает одну операцию за другой, он может обрабатывать только один оператор за раз. Однако такое действие, как запрос данных из API, может занимать неопределенное время, в зависимости от размера запрашиваемых данных, скорости сетевого подключения и множества других факторов. Если вызовы API выполнять синхронно, то браузер не сможет обрабатывать в это время какие-либо другие действия пользователя, например прокрутку или нажатие кнопки, до завершения этой операции.
Это называется блокировкой.Чтобы предотвратить блокирующее поведение, в среде браузера есть множество асинхронных веб-API, к которым JavaScript может получить доступ, что означает, что они могут выполняться параллельно с другими операциями, а не последовательно. Это полезно, поскольку позволяет пользователю продолжать использовать браузер в обычном режиме, пока обрабатываются асинхронные операции.
JavaScript-разработчики должны знать, как работать с асинхронными веб-API и обрабатывать ответ или ошибку в ходе выполнения этих операций. Далее речь пойдёт о цикле событий, взаимодействии с асинхронным поведением с помощью обратных вызовов (callbacks), обещаниях (promises) в ECMAScript 2015 и практике использования async/await
В этой части разберёмся, как JavaScript обрабатывает асинхронный код с помощью цикла событий. Сначала рассмотрим работу цикла событий (event loop), а затем разберём два элемента цикла событий: стек (stack) и очередь (queue).
JavaScript-код, который не использует никаких асинхронных веб-API, будет выполняться синхронно — по очереди, последовательно. Это можно продемонстрировать в следующем примере кода, который вызывает три функции, каждая из которых выводит в консоль число:
function first() { console.log(1) } function second() { console.log(2) } function third() { console.log(3) }
В этом коде определяются три функции, которые с помощью console.log()
выводят числа в консоль. Далее напишем вызовы функций:
first() second() third()
Вывод будет основан на порядке вызова функций: first()
, second()
, затем third()
.
1 2 3
Правила усложняются, когда используется асинхронный веб-API. Встроенный API, с помощью которого это можно проверить — setTimeout
, который устанавливает таймер и выполняет действие по истечении заданного времени. Понятное дело, что метод setTimeout
должен быть асинхронным, иначе браузер останется замороженным во время ожидания, это приведет к плохому взаимодействию с пользователем.
Добавим setTimeout
в функцию second()
для имитации асинхронного запроса:
function first() { console. log(1) } function second() { setTimeout(() => { console.log(2) }, 0) } function third() { console.log(3) }
Метод setTimeout
принимает два аргумента: функцию, которая будет выполнена асинхронно, и время ожидания до вызова этой функции. В этом коде console.log
обернули анонимной функцией и передали её в
, а затем указали вызов этой функции через 0 миллисекунд.
Теперь вызываем функции, как и раньше в том же порядке:
first() second() third()
Можно было бы ожидать, что с setTimeout
, установленным в 0, выполнение этих трёх функций приведёт к тому, что числа будут напечатаны в порядке их вызова. Но поскольку это асинхронно, результат работы функции с таймаутом будет напечатан последним:
1 3 2
Независимо от того, на сколько установлен тайм-аут: ноль секунд или пять минут, не имеет значения — console.log
, вызываемый асинхронным кодом, будет выполняться после синхронных функций верхнего уровня. Это происходит потому, что среда хоста JavaScript, в нашем случае браузер, использует концепцию, называемую циклом событий, для обработки параллельных событий. Поскольку JavaScript может выполнять только один оператор за раз, ему необходимо, чтобы цикл событий был проинформирован о том, когда выполнять конкретный оператор. Цикл событий обрабатывает это с помощью концепций стека и очереди.
Стек (stack)
Стек или стек вызовов хранит состояние того, какая функция в настоящее время выполняется. Концепцию стека можно представить, как массив со свойствами «Последний вошел — первым ушел» (LIFO), то есть можно добавлять или удалять элементы только из конца стека. JavaScript запустит текущий кадр (или вызов функции в определенной среде) в стеке, затем удалит его и перейдет к следующему.
Для примера, содержащего только синхронный код, браузер обрабатывает выполнение в следующем порядке:
- Добавить
first()
в стек, запуститьfirst()
, которая выводит1
в консоль, удалитьfirst()
из стека. - Добавить
second()
в стек, запустить
, которая выводит2
в консоль, удалитьsecond()
из стека. - Добавить
third()
в стек, запуститьthird()
, которая выводит3
в консоль, удалитьthird()
из стека.
Второй пример с setTimout
выглядит так:
- Добавить
first()
в стек, запуститьfirst()
, которая выводит1
в консоль, удалитьfirst()
из стека. - Добавить
second()
в стек, запуститьsecond()
.- Добавить
setTimeout()
в стек, запустить веб-APIsetTimeout()
, который запускает таймер и добавляет анонимную функцию в очередь, удалитьsetTimeout()
из стека.
- Добавить
- Удалить
second()
из стека. - Добавить
third()
в стек, запуститьthird()
, которая выводит3
в консоль, удалитьthird()
из стека. - Цикл событий проверяет очередь на наличие любых ожидающих сообщений и находит анонимную функцию из
setTimeout()
, добавляет функцию в стек, которая записывает2
в консоль, а затем удаляет её из стека.
Использование setTimeout
, асинхронного веб-API, знакомит с концепцией очереди, которую рассмотрим далее.
Очередь (queue)
Очередь, также называемая очередью сообщений или очередью задач, является областью ожидания для функций. Когда стек вызовов пуст, цикл обработки событий проверяет очередь на наличие ожидающих сообщений, начиная с самого старого сообщения. Как только найдётся, оно будет добавлено в стек, который выполнит функцию, указанную в сообщении.
В примере с setTimeout
, анонимная функция запускается сразу после остальной части выполнения верхнего уровня, поскольку таймер был установлен на 0 секунд. Важно помнить, что таймер не означает, что код будет выполняться ровно через 0 секунд или в другое указанное время, а что он добавит анонимную функцию в очередь за это время.
Примечание. Существует ещё одна очередь, называемая очередью заданий или очередью микрозадач, которая обрабатывает обещания (promises). Микрозадачи, такие как обещания, обрабатываются с более высоким приоритетом, чем макрозадания, такие как
setTimeout
.
Теперь вы знаете, как цикл обработки событий использует стек и очередь для обработки порядка выполнения кода. Следующая задача — выяснить, как контролировать порядок выполнения в коде. Для этого сперва рассмотрим оригинальный способ обеспечения правильной обработки асинхронного кода в цикле событий: функции обратного вызова — callback.
Понимание JavaScript: циклы событий и таймеры
В этом блоге Writer’s Room — статьях, написанных членами сообщества Andela — Эбенезер Аджей исследует мир JavaScript, а именно, как использовать циклы событий и таймеры внутри себя!
Задумывались ли вы, как JavaScript — однопоточный язык программирования — обеспечивает асинхронную обработку? Секрет в цикле событий! Как бы вы ни старались, JavaScript как язык может делать только одну вещь в любой момент времени. Не путайте его с многоядерным процессором. Это, однако, не означает, что JavaScript будет ждать вашего вызова API, который занимает 30 секунд, прежде чем запускать следующую строку кода.
JavaScript выполняет наш код сверху вниз и помещает события в стек вызовов и из него с помощью цикла обработки событий. Чтобы веб-браузеры работали более эффективно, у них есть свои реализации в дополнение к движку JavaScript. Например, setTimeout
, setInterval
, fetch API и события DOM являются реализациями браузера (веб-API), а не частью движка JavaScript.
Цикл событий
Цикл событий — важнейший аспект модели времени выполнения JavaScript, который отвечает за выполнение кода, сбор и обработку событий. Это помогает выполнять код асинхронно (неблокируя). Это очень важно в браузерах и на стороне сервера (Node.js). Он основан на модели программирования, ориентированной на события, в которой выполнение программ основано на том, как происходят события, а не на том, как написан код. Представьте себе пользователя, читающего файл с компьютера; программа не зависает, пока не закончится чтение файла. Операционная система читает файл и отправляет событие в очередь, когда чтение файла завершено. Таким образом, программа может реагировать на другие операции ввода-вывода пользователя.
Цикл событий получил свое название из-за того, как он реализован; он продолжает ждать прибытия сообщения, а затем обрабатывает сообщение. Сообщения, добавленные в цикл событий, следуют структуре данных очереди, которая имеет вид First In First Out (FIFO) . В этом случае новые сообщения добавляются в конец очереди, чтобы те, которые вошли в цикл событий первыми, могли быть обработаны первыми.
Вы забыли, как работает очередь? Просто представьте себя в очереди в банке. Вас обслужат последним, если вы присоединитесь к очереди последним. Цикл событий работает так же.
В цикле обработки событий каждое сообщение обрабатывается полностью до обработки любого другого сообщения. Это означает, что после обработки сообщения его нельзя приостановить для обработки другого сообщения. Это позволяет легко следить за выполнением программы. Однако негативным следствием этой модели является то, что когда сообщение занимает слишком много времени, это делает веб-приложение избыточным (например, невозможность щелкнуть или прокрутить).
Использование цикла обработки событий означает, что в очередь могут быть добавлены несколько событий, и они будут обрабатываться так же, как они были добавлены в очередь.
Думали ли вы о том, как новые сообщения добавляются в цикл событий?
В веб-браузерах новые сообщения добавляются на основе происходящих событий. Когда событие создано и к нему присоединен прослушиватель, в цикл событий добавляется новое сообщение. С другой стороны, если прослушиватель отсутствует, событие теряется. Например, при наведении курсора мыши на элемент и присоединении события для обработки добавляется новое сообщение.
Рабочая модель
На изображении ниже показана теоретическая модель среды выполнения JavaScript.
Стек : это часть памяти, зарезервированная для вызовов функций. В стек добавляются новые функции. Когда вызывается новая функция, создается новый кадр стека. Когда функция завершает обработку, она покидает стек.
Куча : Это большая область памяти, зарезервированная для размещения объектов.
Очередь: Содержит сообщения, ожидающие обработки. Когда сообщение завершает обработку, оно покидает очередь сообщений. Каждое сообщение в очереди связано с функцией, которая выполняется для обработки сообщения.
На стороне сервера, обычно в node.js, цикл событий позволяет серверу выполнять неблокирующие операции ввода-вывода. Node.js использует цикл обработки событий для управления операциями и по возможности отправляет их в ядро операционной системы. Как только операции достигают операционной системы, у системы есть собственный способ выполнения задач.
События времени в JavaScript
Таймеры в JavaScript позволяют выполнять код в определенное время. Они включают setTimeout
, setInterval
и установитьНемедленно
. Первые два таймера являются стандартными и широко используются в JavaScript, тогда как последний не является стандартным и реализован только в нескольких браузерах и Node.js.
setTimeout
setTimeout
позволяет нам запускать определенный код через несколько миллисекунд. Он принимает два аргумента; первый аргумент — это функция, которую нужно запустить, а второй аргумент — количество задержек в миллисекундах, прежде чем функция будет помещена в очередь сообщений. Второе значение является необязательным и по умолчанию равно 0. Когда setTimeout
вызывается со вторым аргументом, равным 0, функция будет немедленно помещена в очередь сообщений. В противном случае задержка должна истечь до того, как функция будет помещена в очередь сообщений. setTimeout
функция будет помещена в очередь сообщений сразу после истечения задержки, но не гарантируется, что она будет обработана сразу после истечения задержки. Это связано с тем, что в очереди сообщений уже могут быть другие функции. И так как это очередь, порядок Первый пришел первый ушел (FIFO) . Поэтому задержка, установленная в setTimeout, представляет собой только минимальное время, но не гарантированное время.
Из приведенного ниже примера вы поймете, что функция передается setTimeout
не гарантируется, что он запустится сразу после истечения его времени.
SetTimeout не приостанавливает выполнение
setTimeout
функция является асинхронной, то есть она не приостанавливает выполнение других стеков вызовов. В то время как setTimeout
ожидает определенное количество задержек, другие функции могут выполняться без каких-либо помех. Рассмотрим код ниже:
В приведенном выше примере код не выполняется в том порядке, в котором вызывались функции. Это связано с тем, как setTimeOut
работает внутри. Когда вызывается первая функция, функция обратного вызова не сразу помещается в очередь событий. Вместо этого он будет ждать 5 секунд. Пока это происходит, вызывается вторая функция, которая, в свою очередь, также будет ждать 3 секунды, прежде чем будет помещена в очередь событий. В то время как вторая функция также ожидает, вызывается третья функция, которая ждет 1 секунду, а затем помещается в очередь событий.
Через 1 секунду третий вызов функции помещается в очередь событий.
Через 3 секунды второй вызов функции помещается в очередь событий.
Через 5 секунд первый вызов функции помещается в очередь событий.
Последовательность выполнения приведенного выше кода будет выглядеть следующим образом, и они будут обрабатываться по мере их помещения в очередь событий.
setInterval
setInterval()
Метод, предлагаемый в интерфейсах Window и Worker, неоднократно вызывает функцию или выполняет фрагмент кода с фиксированной задержкой по времени между каждым вызовом. ( источник mdn ). Два метода – setTimeout
и setInterval
— идентичны, за исключением того, что setTimeout
запускает указанную функцию один раз, но setInterval продолжает многократно запускать указанную функцию с заданной задержкой.
Важно отметить, что setTimeout
и setInterval
не являются частью спецификации JavaScript. Но среды, в которых они реализованы, имеют собственный внутренний планировщик, как в случае с браузерами и Node.js.
Прервать выполнение таймеров
Вызов setTimeout
или же setInterval
создает функцию, которая будет выполняться в определенное время.
Вызов функции может быть прерван до выполнения или удален после выполнения.
Когда вызов функции завершается до выполнения, операция удаляется из цикла обработки событий. Это хорошая практика очистить время ожидания
или clearInterval после завершения операции. Опрос идентификаторов, возвращаемый двумя функциями, является общим, поэтому вы можете использовать ясное время ожидания
и clearInterval взаимозаменяемы. Однако, чтобы обеспечить удобочитаемость и ясность, лучше избегать такого обмена.
Пример.
Приведенный выше код будет работать нормально, так как timerID используется совместно с setTimeout
и setInterval
внутри.
Вызов для создания таймера возвращает идентификатор, который можно использовать для отмены выполнения таймера.
Заключение
Несмотря на развитие вычислительной техники и появление многоядерных процессоров, веб-приложения выполняются в одном потоке. Это, однако, не означает, что наши приложения работают блокирующим образом. Важно отметить, что JavaScript никогда не блокирует. Благодаря интеллектуальным методам программирования программы могут работать без сбоев, не блокируя выполнение других событий. Когда обрабатывается I/O или обрабатывается запрос XHR , другие события все еще могут быть обработаны. Есть устаревшие исключения , такие как « предупреждение » и синхронный XHR , которые могут блокировать выполнение других событий. Тем не менее, рекомендуется избегать их.
Хотите стать частью сообщества Andela? Тогда присоединяйтесь к сети талантов Andela!
Найти работу
Если вы нашли этот блог полезным, ознакомьтесь с другими записями нашего блога , чтобы получить более важную информацию!
Похожие сообщения
Понимание setTimeout(). setTimeout() может привести к неожиданным результатам… | Пиюш Кочхар
setTimeout() может привести к неожиданным результатам, если вы не понимаете, как это работает внутри. Давайте узнаем об этом и станем ниндзя setTimeout().
setTimeout()
Это функция в JavaScript, которая задерживает выполнение кода.
Части setTimeout():
Части setTimeout()SetTimeout состоит из 3 частей:
- Обратный вызов
- Операторы, которые должны выполняться внутри обратного вызова
- Время в миллисекундах (время задержки)
Приведенный выше код печатает «Привет» и «Пока» в терминале через 1 секунду (1000 мс = 1 с).
Кажется, это просто! Но если мы не знаем, как setTimeout() ведет себя под капотом, мы можем получить неожиданное поведение.
Пример 1 : Single setTimeout()Вывод :Sum = 3
1 2
Объяснение : В приведенном выше примере setTimeout() имеет задержку 1 секунду. Таким образом, мы получаем сначала сумму, а значения переменных «а» и «б» через 1 секунду.
Example 2: Blocking vs Non-Blocking
Output :We are learning setTimeout
10
20
30
Statements outside are blocking
After 1 second
3
Statements inside are non-blocking
Explanation : setTimeout() — это неблокирующий , что означает, что он будет запущен, когда будут выполнены операторы вне его, а затем через одну секунду он будет выполнен. Все остальные операторы, не являющиеся частью setTimeout(), равны 9.0004 блокирует , что означает, что ни один другой оператор не будет выполняться до завершения текущего оператора. На рисунке ниже показаны блокирующие и неблокирующие операторы.
Блокирующие и неблокирующие операторыЧтобы действительно понять, как работают блокирующие и неблокирующие операторы, нам нужно узнать о стеке вызовов, цикле событий и очереди событий.
Стек вызововОбрабатывает все операторы блокировки и операторы, поступающие из очереди событий.
Цикл событийОбрабатывает все отложенные операторы, например. setTimeout(), setInterval(), события, сетевые вызовы, промисы и все остальные асинхронные операции. Когда таймер setTimeout достигает 0, операторы выходят из цикла событий и переходят в очередь событий.
Очередь событийОбласть ожидания для операторов, которые выходят из цикла событий, и когда весь блокирующий код выполняется стеком вызовов, эти операторы отправляются в стек вызовов для выполнения.
Стек вызовов, цикл событий и очередь событийИтак, давайте посмотрим, как выполняется пример 2.
Шаг 1 : Мы разделяем блокирующие и неблокирующие операторы.
Примечание : Стек вызовов выполняет по одному оператору за раз. Для простоты все операторы блокировки были помещены в стек вызовов сразу.
Вывод : <пусто>
Шаг 2 : Все операторы блокировки в стеке вызовов выполняются последовательно, пока setTimeout() ожидает. Когда setTimeout() ожидает 1 секунду, он перемещается в очередь событий.
Вывод : Мы изучаем setTimeout
10
20
30
Внешние операторы блокируются
Step3 : Операторы выходят из очереди событий и переходят в стек вызовов только тогда, когда весь код блокировки в стеке вызовов выполнено, а стек вызовов пуст. Затем выполняются операторы внутри setTimeout(), и, следовательно, мы получаем наш вывод.
Выход : Мы изучаем setTimeout
10
20
30
Внешние операторы блокируются
Через 1 секунду
3
Операторы внутри не блокируют
Теперь у нас есть некоторое представление о том, как работает setTimeout(). Давайте рассмотрим еще несколько примеров.
Пример 3: Несколько setTimeout()
Вывод :a = 10выше пример легкий. Помните, что все операторы блокировки выполняются первыми, пока все setTimeouts находятся в ожидании цикла событий. Затем каждый переходит в очередь событий один за другим, когда, поскольку 0s имеет самый низкий таймер, он выполняется первым, затем выполняется 1s setTimeout(), затем выполняется 2s setTimeout() и, наконец, 3s setTimeout() выполняется.
b = 20
c = 30
Через 0 секунд
Через 1 секунду
Через 2 секунды
Через 3 секунды
Это выглядит примерно так:
Первый, второй, третий и четвертый setTimeouts выполняются последовательно, когда они выходят из цикла событий в стек вызовов.
Пример 4: Вложенный setTimeout()
Вывод: ноль
шесть
один
два
пять
три
четыре
и «шесть» в качестве нашего вывода в начале, а внешний setTimeout ждет 1 с в цикле событий, затем он переходит в очередь событий, а затем выходит и выполняется, поэтому теперь печатаются «один» и «два». Но мы видим, что «пять» тоже напечатано. Почему? Потому что, как только компилятор JS считывает внутренний setTimeout() (который является неблокирующим оператором) и отправляет его в цикл событий, где он ждет 2 секунды. И пока внутренний setTimeout() ожидает, внешний setTimeout() выполняет все операторы блокировки внутри себя. Следовательно, печатается «пять». А затем, когда внутренний setTimeout() дождался, он попадает в очередь событий и выполняется, так мы получаем «три» и «четыре» в качестве вывода.
На рисунках ниже показано, как работает приведенный выше код:
Пример 5 : Странное поведение 0 и 1 мс.Вывод:
один
ноль
Объяснение : Наш вывод должен был быть «ноль» и «единица», потому что 0 мс меньше 1 мс. Но вместо этого это «единица» и «ноль». Причина в том, что 1 мс — это такой короткий промежуток времени, который в основном эквивалентен 0 мс, поэтому он сразу же переходит в очередь событий из цикла событий и сначала выполняется через стек вызовов.
Пример 6 : Переполнение тайм-аутаВывод: TimeoutOverflowWarning: 2147483648 не помещается в 32-разрядное целое число со знаком.
Длительность тайм-аута была установлена равной 1.
один
ноль
Объяснение : Любое значение времени выше 2³⁰ не помещается в 32-битное целое число со знаком. Итак, компилятор выдает TimeoutOverflowWarning и автоматически устанавливает значение времени равным 1. И из предыдущего примера, поскольку 1 мс в основном эквивалентно 0 мс. Сначала он выполняется, а затем выполняется 0 мс setTimeout().
Пример 7: setTimeout и циклы
Вывод: 3
3
3
Объяснение : Цикл for является блокирующим оператором, поэтому setTimeout() не блокирует. Цикл создает 3 setTimeouts, которые переходят в цикл событий, а затем в очередь событий. Пока все setTimeouts ожидают в очереди событий, значение «i» уже изменилось на 3 в стеке вызовов.