JavaScript: Delay, Sleep, Pause, & Wait
Понимание модели выполнения JavaScript
Прежде чем мы начнём, нужно убедиться, что вы правильно понимаете модель выполнения JavaScript.
Рассмотрим следующий пример кода на Ruby:
require 'net/http'
require 'json'
url = 'https://api.github.com/users/jameshibbard'
uri = URI(url)
response = JSON.parse(Net::HTTP.get(uri))
puts response['public_repos']
puts "Hello!"
Как и следовало ожидать, этот код делает запрос к GitHub API для получения моих пользовательских данных. Затем он анализирует ответ, выводит количество общедоступных репозиториев, связанных с моей учётной записью GitHub, и, наконец, печатает Hello!
на экране. Исполнение идёт сверху вниз.
Сравните это с эквивалентной версией на JavaScript:
fetch('https://api.github.com/users/jameshibbard')
.then(res => res.json())
.then(json => console.log(json.public_repos));
console.log("Hello!");
Если вы запустите этот код, он выведет Hello!
на экран, затем количество общедоступных репозиториев, связанных с моей учётной записью GitHub.
Это связано с тем, что получение данных из API является асинхронной операцией в JavaScript. Интерпретатор JavaScript обнаружит команду fetch
и отправит запрос. Однако он не будет ждать завершения запроса. Скорее, он продолжит свой путь, выводя Hello!
в консоль, а затем, когда запрос вернётся через пару сотен миллисекунд, он выведет количество репозиториев.
Возможно, на самом деле вам не нужны функции sleep
Теперь, когда мы лучше понимает модель выполнения JavaScript, давайте посмотрим, как JavaScript обрабатывает задержки и асинхронные операции.
Создайте простую задержку, используя setTimeout
Стандартный способ создания задержки в JavaScript — использовать метод setTimeout
. Например:
console.log("Hello");
setTimeout(() => { console.log("World!"); }, 5000);
Этот код выведет Hello
в консоль, затем заставит JavaScript ждать пять секунд и выведет World!
. И во многих случаях этого достаточно: сделать что-то, подождать, потом сделать что-то ещё.
Однако имейте в виду, что setTimeout
— асинхронный метод. Попробуйте изменить предыдущий код следующим образом:
console.log("Hello");
setTimeout(() => { console.log("World!"); }, 5000);
console.log("Goodbye!");
Он выведет:
Hello
Goodbye!
World!
Ожидание чего-либо с setTimeout
С помощью setTimeout
(или с его двоюродным братом setInterval
) заставить JavaScript или TypeScript ждать, пока не выполнится условие. Например, так можно использовать setTimeout
для ожидания появления определённого элемента на веб-странице:
function pollDOM () {
const el = document.querySelector('my-element');
if (el.length) {
// Do something with el
} else {
setTimeout(pollDOM, 300); // try again in 300 milliseconds
}
}
pollDOM();
Предполагается, что в какой-то момент элемент появится. Если вы в этом не уверены, вам нужно рассмотреть функцию отмены таймера (используя clearTimeout
или clearInterval
)
Управление потоком в современном JavaScript
При написании JavaScript часто бывает нужно заставить его подождать, пока что-то произойдёт (например, будут получены данные из API), а затем сделать что-то с ответом (например, обновить пользовательский интерфейс для отображения данных).
В приведённом выше примере для этого используется анонимная функция обратного вызова, но если вам нужно дождаться нескольких событий, синтаксис быстро становится довольно корявым, и вы попадаете в ад обратных вызовов.
К счастью, язык значительно изменился за последние несколько лет и теперь предлагает нам новые конструкции, позволяющие избежать ада обратных вызовов.
Например, используя async/away
мы можем написать код для получения информации из GitHub API:
(async () => {
const res = await fetch(`https://api.github.com/users/jameshibbard`);
const json = await res.json();
console.log(json.public_repos);
console.log("Hello!");
})();
Теперь код выполняется сверху вниз. Интерпретатор JavaScript ожидает завершение сетевого запроса и сначала выводится в консоль количество общедоступных репозиториев, а затем сообщение Hello!
.
Если это похоже на то, чего вы пытаетесь достичь, я рекомендую прочитать статью JavaScript: Управление потоком
Переносим sleep
в Нативный JavaScript
Если вы всё ещё со мной, то, полагаю, вы настроены на то, чтобы заблокировать поток выполнения и заставить JavaScript ждать.
Это можно реализовать так:
function sleep(milliseconds) {
const date = Date.now();
let currentDate = null;
do {
currentDate = Date.now();
} while (currentDate - date < milliseconds);
}
console.log("Hello");
sleep(2000);
console.log("World!");
Как и ожидалось, будет выведено Hello
, затем пауза на две секунды и выведется в консоль World!
Функция sleep()
использует метод Date.now
для получения количества миллисекунд прошедших с 1 января 1970 года, и присваивает это значение переменной date
. Затем создаётся переменная currentDate
со значением null
перед циклом do...while
. В цикле повторно запрашивается количество миллисекунд прошедших с 1 января 1970 года, и присваивается переменной currentDate
. Цикл будет продолжаться до тех пор, пока разница между date
и currentDate
будет меньше желаемой задержки в миллисекундах.
Задание выполнено, верно? Ну не совсем…
Улучшенная функция sleep
Возможно, этот код делает именно то, на что вы надеетесь, но имейте в виду, что у него есть большой недостаток: цикл блокирует поток выполнения JavaScript и гарантирует, что никто не сможет взаимодействовать с вашей программой, пока она не завершиться. Если вам нужна большая задержка, есть вероятность, что это может привести к сбою программы.
Так что делать?
Что ж, можно комбинировать методы, описанные ранее в статье, чтобы сделать функцию sleep()
менее навязчивой:
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
console.log("Hello");
sleep(2000).then(() => { console.log("World!"); });
Этот код выводит в консоль Hello
, ждёт две секунды, а затем выводит World!
. Под капотом мы используем метод setTimeout
для разрешения Promise
через заданное количество секунд.
Обратите внимание, что нужно использовать then
, чтобы убедиться, что второе сообщение выведется с задержкой. Так же можно связать больше обратных вызовов с первым:
console.log('Hello')
sleep(2000)
.then(() => console.log('world!'))
.then(() => sleep(2000))
.then(() => console.log('Goodbye!'))
Это работает, но я не большой поклонник всех этих .then()
. Мы можем реализовать это с помощью async/await
.
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
async function delayedGreeting() {
console.log("Hello");
await sleep(2000);
console.log("World!");
await sleep(2000);
console.log("Goodbye!");
}
delayedGreeting();
Это выглядит лучше, но означает, что любой код, использующий функцию sleep()
, должен быть отмечен как async
.
Конечно, у обоих этих методов всё ещё есть недостаток (или особенность), заключающаяся в том, что они не приостанавливают выполнение программы. Только ваши функции sleep
:
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
async function delayedGreeting() {
console.log("Hello");
await sleep(2000);
console.log("World!");
}
delayedGreeting();
console.log("Goodbye!");
Приведённый выше код выведет в консоль следующее:
Hello
Goodbye!
World!
Заключение
Проблемы синхронизации в JavaScript причина головной боли многих разработчиков, и то, как вы с ними справляетесь, зависит от того, чего вы пытаетесь достичь.
Хотя функция sleep
присутствует во многих языках, я бы посоветовал принять асинхронную природы JavaScript и постараться не бороться с этим языком.