Синхронно и асинхронно: в чём разница
Здесь будет встроен видео-разбор темы (5–7 минут, YouTube / RuTube / VK Видео) — тем, кто лучше воспринимает на слух, текст ниже не обязателен.
У JavaScript одна пара рук: он делает ровно одно дело за раз. Поэтому вся разница между синхронным и асинхронным кодом — это разница между «стою и жду» и «займусь другим, позовите, когда будет готово».
Движок выполняет ваш код строка за строкой, в одном-единственном потоке. Пока не досчитана текущая операция, следующая не начнётся — никакого «фонового выполнения» у обычного кода нет. Это упрощает жизнь: значения переменных не меняются сами по себе посреди функции.
Важная деталь: тот же самый поток обслуживает не только ваши вычисления. Он же обрабатывает клики, ввод текста, прокрутку и перерисовку страницы. Занят подсчётом суммы — значит, занят вообще всем.
«Заблокировать поток» — это занять его длинной синхронной работой. Со стороны кода всё нормально: цикл крутится, функция считает. Со стороны пользователя вкладка мертва: кнопка нажимается, но ничего не происходит, буквы не появляются в поле ввода, анимация замирает на месте, а если затянуть — браузер предложит остановить страницу.
Ориентир простой: браузер перерисовывает картинку примерно 60 раз в секунду, то есть на один кадр у него около 16 мс. Синхронная задача длиннее — и кадр пропущен; дольше 100 мс — интерфейс уже ощущается как «подтормаживает».
16 мс — весь бюджет одного кадра. Всё, что дольше, пользователь видит глазами как рывок.
Не всякую работу делает сам JavaScript. Подождать секунду, скачать данные по сети, прочитать файл с диска — этим занимается окружение: браузер или Node.js. У них потоков много, и написаны они не на JavaScript. Ваш код только оставляет поручение и функцию, которую надо вызвать по готовности, — и идёт дальше, не дожидаясь.
Даже нулевая задержка не делает setTimeout мгновенным: колбэк в любом случае попадает в очередь и ждёт, пока поток договорит текущий сценарий до конца.
| Операция | Как ведёт себя |
|---|---|
| Циклы, математика, работа с массивами | синхронно — поток занят вами |
| setTimeout, setInterval | асинхронно — время отсчитывает браузер |
| fetch, загрузка картинки | асинхронно — запрос ведёт сеть браузера |
| fs.readFile в Node.js | асинхронно — чтение ведут служебные потоки Node.js |
| fs.readFileSync в Node.js | синхронно — поток стоит и ждёт диск |
| alert, confirm, prompt | синхронно — замораживают вкладку целиком |
Представьте кафе, где бариста один. Синхронный бариста принял заказ на капучино, нажал кнопку на кофемашине и две минуты смотрит, как та работает. Очередь стоит, хотя сам он ничего не делает. Асинхронный бариста запускает машину, выдаёт номерок, принимает ещё пять заказов, а когда машина пикнула — отдаёт кофе. Кофе сварился ровно за те же две минуты, просто никто не простаивал.
Кофемашина здесь — это браузер, а не второй бариста. И у метафоры есть обратная сторона: если бариста решит десять минут вручную взбивать молоко, никакая асинхронность не спасёт — очередь встанет намертво. Долгий цикл в вашем коде — это ровно оно.
Отсюда честная формулировка: setTimeout(f, 100) означает «не раньше чем через 100 мс», а не «ровно через 100 мс». Колбэк никогда не прерывает выполняющийся код на полуслове.
Три запроса в сеть действительно могут лететь одновременно — их ведёт браузер. Но обработчики их ответов выполнятся по очереди, один за другим, и никогда не столкнутся друг с другом. Асинхронность отвечает на вопрос «когда придёт результат», а не «сколько дел делается разом».
Браузер — это не только движок JavaScript. Внутри него есть сетевой модуль, таймеры, работа с диском и графикой, написанные на C++ и живущие в своих потоках. Единственный поток отдан вашему коду; всё остальное происходит рядом, и вы получаете только готовый результат — в виде вызова вашей функции.
Да — это Web Worker: отдельный поток с отдельным движком, которому можно отдать тяжёлые вычисления. Но общих переменных с основным кодом у него нет, доступа к странице тоже: только обмен сообщениями. Это уже другая тема: живой Worker мы запустим в части II, в разделе «Воркер — это поток, а не страница». А пока достаточно помнить, что по умолчанию поток один.
- JavaScript выполняет код в одном потоке: одновременно делается ровно одна вещь, и этот же поток обслуживает клики и перерисовку страницы.
- Синхронная работа блокирует всё: длинный цикл на секунду — это секунда мёртвого интерфейса; бюджет одного кадра всего около 16 мс.
- Таймеры, сеть и работу с диском выполняет окружение — браузер или Node.js; ваш код оставляет колбэк и идёт дальше.
- Колбэк не прерывает выполняющийся код, а ждёт очереди, поэтому setTimeout(f, 100) значит «не раньше чем через 100 мс».
- Асинхронность не ускоряет саму работу и не даёт параллельности — она лишь не позволяет одному ожиданию тормозить всё остальное.
1/3Что выведет этот код?console.log("A"); setTimeout(() => console.log("B"), 0); console.log("C");
2/3Гарантирует ли setTimeout(f, 1000), что f выполнится ровно через секунду?
3/3Какая из трёхсекундных операций сделает страницу неотзывчивой?
Предскажи порядок вывода
Не запуская код, выпишите, в каком порядке появятся три строки. Потом проверьте себя.
Порядок такой:
Сначала выполняется весь синхронный код — поэтому «сразу» оказывается первым, хотя строка стоит последней. Оба вызова setTimeout только оставили поручение браузеру. Дальше колбэки приходят в порядке готовности таймеров: 0 мс раньше 100 мс.
Убери блокировку
Функция должна поздравлять через две секунды, но сейчас она эти две секунды держит поток занятым — соседняя строка печатается с опозданием. Перепишите так, чтобы вторая строка выводилась немедленно, а поздравление — через две секунды.
Ждать должен браузер, а не движок. Заменяем цикл на таймер:
Обратите внимание: pozdravit() теперь завершается мгновенно — она лишь заказывает вызов на будущее. Ждать те же две секунды никто не перестал, но всё это время поток свободен для кликов и перерисовки.