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