JavaScript: Цикл событий


Введение

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

Стек вызовов и одиночный поток

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

JavaScript является однопоточным языком, что мы все знаем, но что именно это значит? Ну, это значит, что javascript может выполнять только одну задачу за раз, может обрабатывать только один модуль кода за раз, то есть javascript обрабатывает код построчно, по одной строке за раз.

Стеки вызовов записывают, куда обрабатывается наш код. Например, если мы обрабатываем функцию, мы толкаем эту функцию на вершину стека вызовов, а после завершения обработки эта функция выталкивается из стека.

Например:

function a() {
  b();
}

function b() {
  console.log("hi");
}

a();
Войти в полноэкранный режим Выйти из полноэкранного режима

Для приведенного выше кода стек вызовов будет выглядеть следующим образом:

Идея асинхронного выполнения

Однопоточный JavaScript

Итак, теперь мы знаем, что javascript — это однопоточный язык. Он в основном используется для взаимодействия с пользователями и управления элементами DOM.

В Javascript также есть понятие asynchronous и synchronous. С помощью этого механизма он решает проблему блокировки. Здесь мы дадим простое объяснение между этими двумя механизмами.

  • synchronous

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

  • asynchronous

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

Многопоточный браузер

Теперь мы знаем, что javascript является однопоточным, то есть js может выполнять только одну задачу за один раз. Так почему же браузеры могут обрабатывать асинхронные задачи одновременно?

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

Например, если в основном потоке есть запрос на отправку данных, браузер распределит эту задачу на поток Http request thread, затем продолжит выполнение других задач, и когда данные будут успешно получены, он продолжит выполнение callback js кода там, где остановился, а затем распределит задачи callback на основной поток для обработки js кода.

Другими словами, когда вы пишете js-код для отправки запросов данных независимо от протоколов, вы думаете, что это вы отправляете запрос, однако на самом деле это браузер отправляет запрос. Например, для запроса Http, на самом деле запрос отправляет поток http-запросов браузера. Код Javascript просто отвечает за процесс обратного вызова.

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

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

Циклы событий для браузеров

JavaScript классифицирует свои задачи на две категории: синхронные и асинхронные задачи.

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

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

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

execution stack и task queue

Как видно из названия, это стековая структура данных, которая хранит вызовы функций, следуя принципу first-in, last-out (FILO). Он в основном отвечает за отслеживание всего выполняемого кода. Всякий раз, когда выполняется функция, она выгружается из стека; если есть код, который должен быть выполнен, выполняется операция push. Это работает подобно стеку вызовов, о котором говорилось выше.

Опять же, как видно из названия, очередь задач использует структуру данных очереди, которая используется для хранения асинхронных задач и следует принципу first-in, first-out (FIFO). Она в основном отвечает за отправку новых задач в очередь для обработки.

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

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

Весь этот процесс называется event loop.

Макро- и микрозадачи

На самом деле, существует более одной очереди задач. В соответствии с различными типами задач она может быть разделена на micro task queue и macro task queue. Здесь мы перечислим некоторые из наиболее распространенных задач, с которыми вы можете столкнуться, чтобы сформировать более четкое понимание разницы между микро- и макрозадачами.

  • Макрозадачи: script.js (общий код), setTimeout, setInterval, I/O, UI interaction events, setImmediate (среда Node.js).

  • Микрозадачи: Promise, MutaionObserver, process.nextTick (среда Node.js)

Задачи в очередях задач выполняются так, как показано на рисунке ниже:

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

  1. Движок JavaScript сначала берет первую задачу из очереди макрозадач;

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

  3. Затем возьмите следующую задачу из очереди макрозадач. После завершения выполнения снова снимите все задачи из очереди микрозадач, и цикл повторяется до тех пор, пока все задачи в двух очередях не будут сняты.

So to conclude, an Eventloop cycle will process one macro-task and all the micro-tasks generated in this loop.
Вход в полноэкранный режим Выход из полноэкранного режима

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

console.log("sync1");

setTimeout(() => {
  console.log("setTimeout");
}, 0);

new Promise((resolve) => {
  console.log("sync2");
  resolve();
}).then(() => {
  console.log("promise.then");
});

console.log("sync3");
Вход в полноэкранный режим Выйти из полноэкранного режима

Вывод должен быть следующим:

"sync1";
"sync2";
"sync3";
"promise.then";
"setTimeout";
Войти в полноэкранный режим Выйти из полноэкранного режима

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

  1. Когда встречается первая консоль, это синхронный код, который добавляется в стек выполнения, выполняется и выталкивается из стека, и выводится sync1.

  2. Когда встречается setTimeout, это макрозадача и добавляется в очередь макрозадач.

  3. Когда встречается консоль в new Promise, поскольку она решается немедленно, это синхронный код, который добавляется в стек выполнения, выполняется и вытаскивается из стека, и выводится sync2.

  4. При встрече с Promise then, это микрозадача, которая добавляется в очередь микрозадач.

  5. Когда встречается третья консоль, это синхронный код, который добавляется в стек выполнения, выполняется и выталкивается из стека, а sync3 выводится на печать

  6. В этот момент стек выполнения пуст, поэтому выполняются все задачи в очереди микрозадач, и выводится promise.then.

  7. После выполнения задач в очереди микрозадач, выполните одну задачу в очереди макрозадач и выведите setTimeout.

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

На шаге 6 и 7 вы можете быть озадачены тем, почему setTimeout не должен печататься перед promise.then, так как после выполнения console.log("sync3");, он должен сначала просмотреть очередь макрозадач, так как стек выполнения пуст, а затем выполнить все задачи в микрозадачах.

Сложная часть заключается в макрозадаче script. Обратите внимание, что весь код javascript, как в script, является макрозадачей. Более того, это всегда первая макрозадача, которая будет добавлена в очередь макрозадач и первой будет выполнена.

Я уверен, что теперь все понятно. Итак, фактически, после выполнения console.log("sync3");, он указывает, что первая макрозадача выполнена. Таким образом, она продолжит первый раунд Eventloop, заглянув в очередь микрозадач, увидев Promise.then, выполнит ее, и бум! В этот момент первый раунд Eventloop фактически останавливается. Затем второй раунд Eventloop начинается снова, и так далее…

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

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

  • Продолжительность выполнения микрозадачи будет влиять на продолжительность выполнения текущей макрозадачи. Например, во время выполнения макрозадачи создается 10 микрозадач, а время выполнения каждой микрозадачи составляет 10 мс, тогда время выполнения этих 10 микрозадач составляет 100 мс. Можно также сказать, что эти 10 микрозадач вызвали задержку макрозадачи на 100 мс.

  • Существует только одна очередь макрозадач, и каждая макрозадача имеет свою очередь микрозадач. Таким образом, каждый раунд Eventloop состоит из одной макрозадачи + нескольких микрозадач.

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

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

setTimeout(function () {
  console.log(" set1");
  new Promise(function (resolve) {
    resolve();
  }).then(function () {
    new Promise(function (resolve) {
      resolve();
    }).then(function () {
      console.log("then4");
    });
    console.log("then2 ");
  });
});

new Promise(function (resolve) {
  console.log("pr1");
  resolve();
}).then(function () {
  console.log("then1");
});

setTimeout(function () {
  console.log("set2");
});

console.log(2);

new Promise(function (resolve) {
  resolve();
}).then(function () {
  console.log("then3");
});
Вход в полноэкранный режим Выйти из полноэкранного режима

Ваш ответ должен выглядеть следующим образом:

pr1
2
then1
then3
set1
then2
then4
set2
Войти в полноэкранный режим Выйти из полноэкранного режима

Оцените статью
devanswers.ru
Добавить комментарий