Comments 53
спам
Здравствуйте! Если вам показалось, что в статье что-то лишнее или недостаточно полезное, буду рад узнать, что именно, чтобы улучшить материал. Конструктивная критика всегда приветствуется!
С промисами не надо играться. Самое первое что надо сделать - это прочитать и понять документацию. И только потом начинать что-то делать. Тут простым тыканием setTimeout не обойтись.
Спасибо за ваш комментарий! Я согласен, что документация - важная часть изучения любой технологии. Однако, на мой взгляд, простые эксперименты (в том числе с setTimeout) тоже дают очень ценное понимание того, как работает асинхронность. Документация помогает увидеть "официальную" картину, а игры с кодом помогают прочувствовать все механизмы на практике и лучше понять, какие возникают проблемы и почему. Так что, как часто бывает, лучший путь - это сочетание теории и практических экспериментов.
По теории вероятности, если миллиону обезьян дать пишущую машинку, то рано или поздно они напишут "Войну и Мир". Развитие интернета доказало, что это не так. Не мое) Смотреть в консоль и запомнить как работает таймаут и действительно понять как это работает - разные вещи. Есть же куча нюансов - обработка ошибки, возврат значений по цепочке, finally и т.д.
Если синор пишет код ёлочками-то он джун. Должна быть определенная стилистика, понимание dry и хотя б первой буквы в solid, если наложить ограничения на сложность и вложенность и понимать самодокументируемость- то новичок не запутается в ваших еловых лесах так как не будет явной восьмикратной вложенности лямбд. Многопоточка тут не причем.
Если у меня на be джун запутается в лямбдах- то это скорее моя вина, я не заблочил MR с лапшой и ёлками.
Хорошая статья, всё сжато и легко читается, спасибо Автору!
Когда промис переходит из pending в
resolvedили rejected.
fulfilled, наверное, правильно
Вы абсолютно правы, термин "fulfilled" действительно точнее описывает успешное завершение промиса. Спасибо за уточнение! Исправил этот момент!
Ну в JS используется именно термин resolve (стоит глянуть хотя бы на название метода). Но да, fulfill было бы благозвучнее. =)
resolve скорее описывает действие.
Зарезолвите промис и выведите его в консоль, чтобы увидеть состояние. Там будет fulfilled.
Но это всё буквоедство, все и так всё поняли. Перестаю нудеть.
Фокус в том, что промис может быть уже resolved, но ещё pending. Для этого достаточно зарезолвить его другим промисом в этом состоянии. Потому и требуется отдельный термин для финального состояния.
Если Джун путается, значит это не Джун а трейни
Всё
Пипец обиженок набежало... Правда глаза колит? На работе нужно не только пить смузи и получать зарплату, но и работать
Идеальный программист должен быть достаточно умным, чтобы не путаться в асинхронизмах, но при этом достаточно тупым, чтобы согласиться работать за джуниорскую зарплату.
Я занимаюсь преподаванием JS, и как мне кажется основная проблема с усвоением материала по асинхронному кодингу в том, что мы выливаем на студента слишком много слишком быстро - (коллбеки, промисы, async/await). Я обычно объясняю как работает fetch и даю студенту поиграться с сервером исходный код которого от него пока скрыт.
Он отправляет два запроса (а сервер отвечает на запросы случайное время) и студент видит что что-то идёт не так, при этом он находится в своей кодовой базе и его не смущают искусственные вставки в его код всяких сет таймаутов.
Далее я показываю коллбеки, прошу его посчитать в конце кода сумму полученную с сервера. Ведь проблема колбэков не только в коллбэк хелле, но и в то что с ними тупо неудобно. Ну и далее промисы и асинки. И только потом я показываю что там было на сервере.
Вспоминая студенческие сложности скажу. Если бы я снова учился асинхронщине, то я бы хотел что бы это выглядело как-то так.
Сначала необходимо подробно объяснить связь и разницу понятий многопоточность, асинхронность и параллельность. Объяснить про потоки операционной системы и про легковесные потоки (зелёные треды, горутины, корутины, как там это в вашем языке называется?) и пулы потоков. Потом объяснение какие инструмены для работы со всем этим есть в вашем языке. Рассмотрение какие проблемы использование асихронности решает. Проблемы использования данных инструментов - дедлоки, гонки данных. Простейшие шаблоны для данных инструментов. Ну и самое главное на каждом шаге - это задачи. Люди думают, что если расскажут про асинхронный запуск операций, то их работа по введению в асинхронность закончена. Даже если студент посмотрит вам в глаза и честно скажет "понятно", то когда приступит к решению задач, то поймёт, что "не понятно". Мало кто пишет такие задачи на асинхронность. Легче дать копипастную задачу на алгоритмы
Спасибо за ваш опыт! Полностью согласен, что лучше вводить асинхронность постепенно и "показывать магию" в небольших примерах, чтобы студенты сами прочувствовали, как всё работает. Так они осознают, почему колбэки неудобны, как промисы упрощают жизнь, и наконец приходят к async/await с чётким пониманием "что" и "почему".
А в чем неудобство коллбэков?
Вложенность (callback hell). Много уровней колбэков, код уезжает вправо и становится нечитабельным.
Сложная обработка ошибок. Приходится вручную передавать и обрабатывать ошибки в каждом колбэке.
Трудности с управлением потоком. Параллельные и последовательные задачи требуют дополнительных ухищрений (счётчики, ручная синхронизация и т. д.).
Невозможность вернуть значение "наверх". Результат появляется только внутри колбэка, что сбивает с толку и усложняет структуру программы.
Хорошая статья. Я бы ещё упомянулPromise.allSettled
- он удобнее при обработки ошибок, чем Promise.all
.
Насчёт того, почему новички путаются с асинхронным кодом. Всё-таки JS многое прощает до определенного момента и не все разработчики привыкли мыслить "вглубь" при необходимости. Здесь, кстати, помогает хорошее высшее образование, после базового курса параллельного и асинхронного программирования на C/C++ такие вещи хорошо чувствуются.
Спасибо за обратную связь!
Согласен, что Promise.allSettled
очень удобен для обработки ошибок, когда важны результаты всех запросов.
Ну это же два принципиально разных метода! all используется для случая "все или ничего" allSettled для случая "дай хоть что-то"
Спасибо, очень полезный материал. Понравилось доступное изложение, хотя тема не из лёгких.
Вот чего мне не хватает в JavaScript, так это простого способа приостановки выполнения кода вроде функции sleep (n). Мне приходилось разрывать линейный код на несколько разных функций с вызовом через setTimeout (), ну, или, что то же самое, с помощью Promise. Или есть всё-таки какое-нибудь простое решение?
Наиболее близкий к классическому sleep способ в JavaScript - это промис с setTimeout
, который удобно использовать вместе с async/await
. Например:
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
async function doSomething() {
console.log('Начинаем...');
await sleep(2000); // спим 2 секунды
console.log('Продолжаем!');
}
Такой подход позволяет писать код последовательно (как будто действительно есть sleep), не разрывая логику на несколько колбэков. Синтаксически это самое простое решение, поскольку "настоящей" блокирующей паузы (как в других языках) в JavaScript нет - в нём однопоточная модель с циклом событий, и принудительная остановка потока в целом не предусмотрена.
Главная проблема асинхронного кода заключается в том, что в 99% случаев он разработчику не нужен, но разработчика заставляют использовать асинхронщину в принудительном порядке, ибо платформа/фреймворк по-другому попросту не умеет и лишена соответствующего функционала.
Конкретно в случае браузерного JS первопричиной появления асинхронной лапши является невозможность читать внешнее состояние прямо на месте, без обязательного выхода из текущей функции. Ну то есть вы не можете сделать что-то вроде
if ( isKeyPressed(KEY_SPACE) ) { doSomething() }
вам непременно придётся сперва сделать return явно или неявно, а потом вернуться обратно. У вас нет доступа к очереди событий вашей же собственной странички, вы не можете опрашивать мышь с клавиатурой, да вообще ничего "снаружи" читать не можете без предварительного return'а. В итоге логику приходится нарезать на мелкие кусочки крайне противоестественным образом.
При этом не существует ни одной разумной причины для подобного говнодизайна API. Так, например, браузерные API для работы с геймпадом сделаны по-человечески - вы можете считать состояние кнопок прямо на месте, без возвратов и промисов. И насколько же красивее, проще и читабельнее сразу выглядит код! Прямо сидишь и удивляешься - а что, так можно было? А почему с мышью и клавиатурой не так? Какой идиот придумал API именно такими, чтоб без асинхронки ими было невозможно пользоваться?
В последнее время разработчики браузеров видимо начали подозревать, что здесь что-то не так, потому что добавили async/await. Вот только добавили их максимально кривым и костыльным способом - в виде синтаксического сахара поверх всё тех же промисов. Проблему не решили, а замели под ковёр, откуда она время от времени вываливается.
Спасибо за ваши мысли! :)
Наверное, важно упоминуть, что исторически браузерный JavaScript создавался как событийно-ориентированный язык, чтобы пользовательский интерфейс не блокировался во время выполнения кода. Это обусловлено однопоточностью: если бы код мог синхронно ждать результат какого-нибудь действия (например, сетевого запроса), вся страница «зависала» бы до получения ответа, и взаимодействие с ней было бы невозможным. Асинхронная модель здесь не просто дань моде, а способ обеспечить отзывчивость и избежать блокировок.
Почему с геймпадом можно обойтись опросом "на месте", а с клавиатурой и мышью - нет? Геймпад API добавляли позднее, учитывая особенности игр (где логика кадра и прямой опрос железа - обычная практика). Клавиатура и мышь в браузере изначально пошли другим путём - событием: нажатие, отпускание, движение. Это наследие DOM-модели и всего, что в ней связано с обработкой пользовательских действий, фокусом и безопасностью.
Наконец, async/await хотя и является синтаксическим сахаром над промисами, всё-таки упрощает написание асинхронного кода. Важно понимать, что асинхронность в JS востребована не только при обработке событий ввода, но и при работе с сетью, файлами, базами данных. В условиях одного потока это помогает не подвешивать UI, пока мы ждём ответ от сервера или читаем данные из indexDB.
Я согласен, что браузерные API не идеальны, и в некоторых местах они выглядят как исторический атавизм, но ИМХО менять их радикально уже поздно: слишком много кода написано в расчёте на текущий подход. В итоге мы имеем модель, которая может показаться избыточно сложной, однако она решает фундаментальную проблему блокировок и сохраняет гибкость для дальнейшего развития платформы.
Вот кстати про http запросы, браузеры все ещё поддерживают синхронный XMLHttpRequest, но грозят удалить (оставив только в воркерах), типо это плохо для UX. Хотя я не спорю, что это плохо для UX, но иногда это оправдано. Допустим огромный легаси код, который был написан синхронно, или какая-то функция в библиотеке, где часть логики перенесена в бэкенд. Некоторые фрейморки тоже используют это.
Я думаю нельзя так агрессивно впихать асинхронность и решать все за разработчиков, так как опытные разработчики понимают последствия и компромиссы всех своих решений.
"мы выливаем на студента слишком много слишком быстро"
Что ж они такие нежные, по хорошему надо дать теорию, ответить на вопросы если кому что не ясно, и вываливать задачу сразу на Rust с tokio, там компилятор просто не даст сделать ерунды, и пока не скомпилируется пусть разбирается что не так, когда учатся на своих ошибках - на дольше запоминается)))
Можно было сделать по-другому даже в JS. Например:
(()->{обычный код;}).fork(приоритет);
Как это могло бы работать? Напишу по мотивам другого языка, где всё это реализовано:
Пусть у нас есть "зелёные" потоки (т.е., используют кооперативную многозадачность, и де-факто выполняются в одном аппаратном потоке). В каждую единицу времени выполняется только один "зелёный" поток (назову его "активным"), остальные ждут, когда до них дойдёт очередь выполняться.
И есть функции FFI (foreign function interface), некоторые из которых выполняются в других аппаратных потоках (обозначу их TFFI, threaded FFI). Эти TFFI разумно создавать для чего-то потенциально длительного (запросы к вебсерверам, базам данных, просто задания паузы на заданное время...).
Когда некий "активный" "зелёный" поток (назову его T1) вызовет какую-нибудь функцию TFFI, тем самым он сразу переводится в состояние ожидания, а выполняться начнёт какой-то другой "зелёный" поток - в соответствии с приоритетом и пр. (Когда же та функция TFFI наконец выполнится и T1 получит свой "долгожданный" результат, он не продолжит выполнение автоматически, а просто попадёт в пул потоков, ждущих своей очереди для выполнения).
Сам по себе JS-код выглядел бы как обычный многопоточный код, который намного проще понимать и писать, чем описанное выше.
Под капотом лежит очень несложный механизм параллелизма, обеспечиваемый архитектурой ОС. Мне кажется, что самое правильное - это разобраться с ним, а после пробиться через все "упрощения", возведённые поверх базовых механизмов создателями конкретного языка/платформы, и построить отображение одного в другое.
Получается как в известном стихотворении про мат: 'Слов немного, всего лишь пяток. Но какие из них сочетания!..'
Спасибо за мнение! Архитектура ОС действительно лежит в основе параллелизма. Но для веб-разработчиков важно сначала освоить практическое применение асинхронности в рамках Event Loop и API языка. ИМХО углубление в базовые механизмы хоть и полезно, но не всегда обязательно для начального понимания.
Возможно, это и вкусовщина, но по мне без понимания базы довольно трудно понять, что из этого выросло. Конечно, с соблюдением подходящего уровня абстракции. В результате понимание/освоение новых технологий проходит менее болезненно. В противном случае этот самый джун потом снова будет долго и упорно изучать немного другой подход к тому же самому снаряду. Это важно, если человек хочет быть программистом, а не спецом по конкретной платформе.
Хоть и на js, но всё равно было полезно)
Полезный обзор асинхронности без лишней воды. Тем, кто уже шарит в теме ничего нового, но новичкам пригодится для систематизации знаний 👍🏻
Имхо, новичкам бы ещё пригодился абзац другой про отличия async/await промисов, да с парой примеров, где что лучше использовать.
Спасибо за статью! Добавил в закладки.
Статья для новичков полезна! Помню свою радость когда появились async/await (так ждал их после работы на C#)
Спасибо, как раз столкнулся с этим вопросом в работе.
Статья супер, большое спасибо автору!
отличная статья, спасибо
Почему джуны путаются в асинхронном коде (и как научиться с ним работать)