Введение
JavaScript, язык Интернета, предоставляет разработчикам мощные инструменты для создания интерактивных и динамических веб-приложений. Однако его однопоточная природа иногда может привести к путанице при работе с параллельными задачами. В этом блоге мы углубимся в цикл событий JavaScript, поймем, как он обрабатывает параллелизм, и изучим методы оптимизации производительности. Давайте разгадаем тайну цикла событий.
Стек вызовов и контексты выполнения
JavaScript отслеживает текущую задачу или функцию в стеке вызовов. При каждом вызове функции создается новый контекст выполнения, содержащий переменные, аргументы и кодовую позицию. Функции помещаются в стек по мере их выполнения, а когда они завершаются, они извлекаются из стека. Например,
function f1(){ function f2(){ function f3(){ } f3(); } f2(); } f1(); // call stack => f1 - f2 - f3 <- HEAD
Как вы, возможно, знаете, как элементы добавляются и извлекаются из стека с помощью механизма LIFO (Last In First Out). Таким образом, функции будут появляться в порядке f3
-> f2
-> f1
.
Синхронное выполнение кода: проблемы и решения
Это синхронный способ обработки выполнения, который означает, что код выполняется последовательно одно за другим, не заботясь о том, что некоторые задачи выполняются медленно.
Как вы, наверное, уже догадались, это неправильный способ эффективного выполнения программы. Должен быть способ выполнить часть кода без этого блокирующего поведения (медленные процессы блокируют выполнение другого кода).
Решением может быть то, что мы берем медленный код и выполняем его в каком-то потоке, отличном от основного, чтобы остальные задачи могли выполняться без какого-либо времени ожидания. Но движок V8 (среда выполнения JavaScript) является однопоточным, что означает, что он может выполнять только одну задачу за раз, так как же мы можем этого добиться?
Вы слышали о функции setTimeout()
? Если нет, то это просто функция, которая принимает другую функцию и время в миллисекундах в качестве своего параметра, и функция выполняется через указанное время. Да! он неблокирующий. Но как он может работать параллельно, когда у нас есть только один поток? Оказывается, этот setTimeout()
не содержится в V8 и предоставляется средой, в которой размещается V8, которая является браузером или узлом (если вы работаете на стороне сервера)!
Асинхронность и очереди обратного вызова
В основе асинхронного программирования лежит взаимосвязь между текущей и последующей частями вашей программы. В приведенном выше примере мы берем функцию внутри setTimeout()
кадра стека из стека вызовов и перемещаем ее куда-нибудь, чтобы ее можно было выполнить позже, через указанное время. Это ожидание в течение указанного времени обрабатывается хостом в другом потоке, отличном от основного потока.
Через x миллисекунд функция обратного вызова будет добавлена хостом в очередь задач. Когда стек вызовов пуст, обратные вызовы в очереди задач будут удалены из очереди и добавлены в стек для выполнения. Удаление из очереди и выполнение из очереди задач не блокирует добавление новых задач в стек вызовов, поэтому, если в стек вызовов добавляются новые элементы, очередь задач ожидает, пока стек вызовов не станет пустым.
Давайте рассмотрим несколько примеров, чтобы лучше понять это,
- Когда мы выполняем код ниже, цикл событий выполняет цикл while, и он никогда не завершится. Вы можете видеть, что экран не будет реагировать ни на что, что вы делаете. Это связано с тем, что цикл событий занят выполнением задачи ниже, которая никогда не завершается, а задача, которую вы вводите, поставлена в очередь за кодом ниже, который никогда не будет достигнут.
document.querySelector("button").addEventListener("click", () => { while (true); });
- Угадайте, что произойдет, когда вы нажмете на кнопку кода ниже. После нажатия вы можете увидеть, что экран реагирует, но только на 5 секунд. Потому что при встрече с
setTimeout()
хост будет ждать 5 секунд, и, поскольку он не блокирует, другие вещи выполняются во время ожидания. Через 5 секунд обратный вызов помещается в очередь задач, и, поскольку стек вызовов пуст, выполняется цикл while, и это блокирует все.
document.querySelector("button").addEventListener("click", () => { setTimeout(() => { while (true); }, 5000); });
- Приведенный ниже код будет немного сложным для понимания, если вы новичок в этом. При нажатии на кнопку браузер ждет заданное время, а тут ноль секунд, что? Разве это не то же самое, что и синхронное выполнение без
setTimeout()
? Здесь следует отметить, что на самом деле это не 0, а 4 мс, помните, что функция внутриsetTimeout()
будет перемещена в очередь задач независимо от того, что произойдет. Таким образом, задачи в очереди задач не будут исключены из очереди, если стек вызовов не будет пуст. Таким образом, мы можем сказать, что использованиеsetTimeout(..., 0)
— это хакерский способ асинхронного выполнения. Однако выполнение приведенного ниже кода, несмотря на бесконечные рекурсивные вызовыsetTimeout()
, не блокирует. Попробуйте подумать, почему!
document.querySelector(".button").addEventListener("click", () => { function sample() { setTimeout(() => { foo(); }, 0); } foo(); });
Какие-то проблемы?
У обратных вызовов есть несколько недостатков, которые ограничивают их использование каждой асинхронной операцией. Наиболее важные перечислены ниже,
- При работе с несколькими асинхронными операциями использование обратных вызовов может привести к глубокой вложенности структур кода, что затруднит чтение и поддержку кода. Это называется Callback Hell.
- Когда обратный вызов обрабатывается третьей стороной, мы вызываем функцию и доверяем им, что они будут правильно вызывать обратные вызовы. Эта инверсия контроля может привести к проблемам с доверием.
- Когда несколько асинхронных операций инициируются и изменяют общие данные, порядок их выполнения и результирующие значения могут быть недетерминированными (состояние гонки). Это может привести к конфликтам и несогласованным состояниям, если операции не синхронизированы должным образом.
- Обработка ошибок становится громоздкой с обратными вызовами, так как вам нужно передавать параметры ошибки вместе с результатами успеха. Это может привести к повторяющемуся коду проверки ошибок в каждом обратном вызове.
Заключение
Понимание цикла событий и того, как JavaScript обрабатывает параллелизм, имеет решающее значение для написания эффективных и отзывчивых веб-приложений. Используя цикл событий, разработчики могут использовать возможности асинхронного программирования, эффективно обрабатывать события и оптимизировать производительность. Воспользуйтесь циклом событий и раскройте истинный потенциал JavaScript!
Я надеюсь, что эта запись в блоге поможет вам глубже понять цикл обработки событий и его роль в параллелизме JavaScript. Будущие блоги будут посвящены улучшению взаимодействия с пользователем за счет смягчения воздействия ресурсоемких задач, Promises в Javascript и новому асинхронному ожиданию.
Удачного кодирования!