В этой статье мы рассмотрим, как работает javascript изнутри, как он выполняет наш асинхронный код javascript и в каком порядке (Promise vs setTimeout), как он генерирует трассировку стека и многое другое ..

Как известно большинству разработчиков, то, что Javascript является однопоточным, означает, что два оператора в javascript не могут выполняться параллельно. Выполнение происходит построчно, что означает, что все операторы javascript синхронны и блокируются. Но есть способ запустить ваш код асинхронно, если вы используете функцию setTimeout(), веб-API, предоставляемый браузером, который гарантирует, что ваш код выполняется по истечении указанного времени (в миллисекундах). Пример кода:

console.log('Message 1');
// Print message after 100 millisecond
setTimeout(function() {
   console.log('Message 2');
}, 100);
console.log('Message 3');

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

После выполнения вышеуказанных операторов браузер сначала распечатает «Сообщение 1» и «Сообщение 3», затем он распечатает «Сообщение 2». Здесь вступает в действие цикл событий, который гарантирует, что ваш асинхронный код будет выполняться после того, как весь синхронный код будет выполнен.

Визуализация цикла событий

Я создал структуру цикла событий, используя HTML и CSS. Вы можете проверить это на Codepen:

Стек: здесь весь ваш код javascript помещается и выполняется один за другим, когда интерпретатор читает вашу программу, и выскакивает после завершения выполнения. Если ваш оператор является асинхронным: событие setTimeout, ajax(), promise или click, то этот код перенаправляется в таблицу событий, эта таблица отвечает за перемещение вашего асинхронного кода в очередь обратного вызова / событий по истечении указанного времени.

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

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

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

Очередь заданий: помимо очереди обратного вызова браузеры представили еще одну очередь, которая называется «Очередь заданий», зарезервированная только для new Promise() функций. Поэтому, когда вы используете обещания в своем коде, вы добавляете метод .then(), который является методом обратного вызова. Эти методы thenable добавляются в очередь заданий после того, как обещание было возвращено / разрешено, а затем выполняются.

Быстрый вопрос: например, проверьте эти утверждения, можете ли вы предсказать последовательность вывода ?:

console.log('Message no. 1: Sync');
setTimeout(function() {
   console.log('Message no. 2: setTimeout');
}, 0);
var promise = new Promise(function(resolve, reject) {
   resolve();
});
promise.then(function(resolve) {
   console.log('Message no. 3: 1st Promise');
})
.then(function(resolve) {
   console.log('Message no. 4: 2nd Promise');
});
console.log('Message no. 5: Sync');

Некоторые из вас могут ответить на это:

// Message no. 1: Sync
// Message no. 5: Sync
// Message no. 2: setTimeout
// Message no. 3: 1st Promise
// Message no. 4: 2nd Promise

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

// Message no. 1: Sync
// Message no. 5: Sync
// Message no. 3: 1st Promise
// Message no. 4: 2nd Promise
// Message no. 2: setTimeout

Сначала вызываются все обратные вызовы `thenable` обещания, затем вызывается обратный вызов setTimeout.

Почему? Очередь заданий имеет высокий приоритет при выполнении обратных вызовов. Если тик цикла событий поступает в очередь заданий, она сначала выполняет все задания в очереди заданий, пока она не станет пустой, а затем перейдет в очередь обратных вызовов.

Если вы хотите глубоко погрузиться в то, почему promises вызывается до setTimeout, вы можете ознакомиться с этой статьей Задачи, микрозадачи, очереди и расписания от Джейка Арчибальда. Что действительно хорошо это объясняет.

Примечания по выполнению кода

  • Ваш асинхронный код будет запущен после того, как «Главный стек» будет выполнен с выполнением всей задачи.
  • Это хорошая часть: ваши текущие операторы / функции в стеке будут выполняться до конца. Асинхронный код не может их прервать. Как только ваш асинхронный код будет готов к выполнению, он будет ждать, пока основной стек не станет пустым.
  • Это также означает, что не гарантируется, что ваш setTimeout() или любой другой асинхронный код будет работать точно после указанного вами времени. Это время - минимальное время, по истечении которого ваш код будет выполняться, оно может быть отложено, если основной стек занят выполнением существующего кода.
  • Если вы используете время 0 мс в своем setTimeout, он не запустится немедленно (если основной стек занят). например:
setTimeout(function() {
   console.log('Message 1')
}, 0);
console.log('Message 2');

В приведенном выше примере первым выводом будет «Сообщение 2», затем «Сообщение 1», даже если setTimeout установлен на запуск через 0 миллисекунд. Как только браузер обнаруживает setTimeout, он выталкивает его из основного стека в очередь обратного вызова, где он ожидает, пока основной стек завершит второй console.log, затем setTimeout возвращается в основной стек и запускает первый console.log.

  • Если вы выполняете слишком много тяжелых вычислений, это приведет к тому, что браузер перестанет отвечать, потому что ваш основной поток заблокирован и не может обрабатывать другие задачи. Таким образом, пользователь не сможет щелкнуть по вашей веб-странице. В этом случае браузер выдает ошибку «Сценарий занимает слишком много времени для выполнения» и дает вам возможность «убить скрипт» или «подождать».

[Необязательно] Ошибка StackTrace

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

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

function func1 () {
  // Accessing undefined variable will throw error
  console.log(err);
}
function func2 () {
 func1();
}
function func3 () {
 func2()
}
// Calling func3, will result in error in func1
func3();

Как видно из трассировки стека ошибок, ошибка возникла в функции func1, которая была вызвана в строке №. 7 в func2, а затем func2 был вызван в func3 в строке №. 11.

Когда теперь следует использовать цикл обработки событий?

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