JavaScript: Delay, Sleep, Pause, & Wait

Источник: «Delay, Sleep, Pause, & Wait in JavaScript»
Многие языки программирования имеют функцию sleep, которая задерживает выполнение программы на заданное количество секунд. Однако эта функциональность отсутствует в JavaScript из-за его асинхронной природы. В этой статье мы рассмотрим почему это произошло, а затем как реализовать функцию sleep самостоятельно.

Понимание модели выполнения 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.

See the Pen

Конечно, у обоих этих методов всё ещё есть недостаток (или особенность), заключающаяся в том, что они не приостанавливают выполнение программы. Только ваши функции 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 и постараться не бороться с этим языком.

Дополнительные материалы

Предыдущая Статья

JavaScript: Управление потоком

Следующая Статья

JavaScript: Что такое Промисы / Promise