Языки программирования подразделяются на однопоточные и многопоточные. Но что такое поток? Поток – это последовательность выполнения транслятором инструкций. Представьте, что вы транслятор, а потом – это книга, где каждая страница – это инструкция. И вот вы листая книгу, выполняете инструкцию за инструкцией. Вот так примерно и работает программа в однопоточном режиме. А вот если бы у вас было две головы, вы уже бы могли читать две книги параллельно. А Если три головы, то три книги и т.д. Это уже был бы многопоточный режим выполнения программы.
JavaScript относится к однопоточным языкам. И вы можете сказать, что JavaScript значит хуже чем, например, С#, который поддерживает многопоточность. Дело в том, что хоть и JavaScript относится к однопоточным языкам программирования, но он обладает псевдо параллелизмом. Почему я сказал псевдо, вы скоро узнаете.
Параллелизм – это возможность выполнять несколько операций одновременно. Но как такое возможно в одном потоке?
Представим, что у вас есть программа, которая должна запустить последовательно столько инструкций. И там в качестве четвертой инструкции у нас есть функция setInteval, которая будет запускать допустим сообщение в консоль, через каждые две секунды.
Транслятор дойдя до этой инструкции, выведет сообщение в консоль, а затем запомнит, что надо вывести еще раз сообщение в консоль через две секунды. Дальше транслятор начинает выполнение 5, 6 и т.д. инструкций. И вот прошло 2 сек, и транслятор, допустим за это время добрался до 12 инструкции. Конечно, в реальности, транслятор выполняет инструкции гораздо быстрее. Но так думаю будет более понятнее. Итак, прошло две секунды, транслятор добрался до 12 инструкции, но он помнит, что ему надо вывести сообщение в консоль. Что он делает? Он сдвигает все инструкции которые не выполнил на шаг вниз, теперь инструкция которая была по счету 12ой, стала 13-ой и т.д. А в качестве 12ой инструкции он вставляет вывод в консоль сообщения. Транслятор выполнит ее и двинется дальше.
А что будет, если 11 функция будет сильно сложной, и на ее выполнение понадобится целых 3 секунды? Тогда транслятор также сдвинет инструкции начиная с 12 вниз на один шаг, в качестве 12 инструкции он поставит вывод в консоль. Но не выполнит ее пока не выполнит 11 инструкцию, т.е. хоть мы и задали, чтобы вывод в консоль совершался каждые две секунды, но в данном моменте вывод в консоль совершится примерно через 5 секунд. Это конечно минус однопоточного режима выполнения в сравнении с многопоточным. Но так как Транслятор обрабатывает код гораздо быстрее, чем я рассказал в примере, такие ситуации с серьезными задержками происходят очень редко. Теперь вы знаете как происходит параллельное выполнение кода в однопоточном режиме.
Поэтому я и сказал, что JavaScript обладает псевдо параллелизмом. На самом деле функция SetInterval запускается асинхронно, не блокируя выполнение основного потока, т.е. если выполняется сложная функция в основном потоке, то SetInterval подождет, когда она закончит выполнение, и выполнится вслед за ней. Т.е. JavaScript использует не параллелизм, а асинхронность для имитации выполнения двух и более функций одновременно. Но так как во многих учебниках и видеокурсах, говорят именно параллелизм, то я также этим термином назвал эту лекцию, но все-таки правильней говорить асинхронное выполнение кода, вместо параллелизм.
Давайте теперь подробнее поговорим про асинхронность в JavaScript. Асинхронное выполнение кода в JavaScript можно создать 4 способами. В этом уроке мы с вами рассмотрим пока только один способ. Остальные освоим уже на следующих уроках.
Первый способ – это с помощью колбеков. С комбеками вы уже сталкивались и это просто функции, которые вызываются внутри других функций. Например, у нас есть функция с writeToConsole, внутри которой находится метод консоль log. Далее создаем функцию callWriteToConsole, которая просто вызовет функцию WriteToConsole. Получается функция WriteToConsole вызывается внктри callToWriteToConsole, и значит она является колбеком для функции callToWriteToConsole. Но сами по себе колбеки не выполняются асинхронно, так как пока не выполнится колбек WriteToConsole, остальной код в функции CallToWriteToConsole , если он есть, тоже не будет выполняться.
Но мы можем добавить асинхронное поведение колбеку с помощью уже знакомой функции SetInteval, а также setTimeout, который позволяет запустить единожды колбек через определенное время. Т.е. когда мы создаем внутри SetInterval или SetTimeout функцию, она автоматически является колбеком так как она вызывается внутри SetInterval или SetTimeout. И этому колбеку функции SetInterval и SetTimeout придают асинхронное поведение. Т.е. сами по себе эти две функции не обладают асинхронным поведением, но они могут придать это поведение своим колбекам.
Вот так с помощью колбеков и функций SetInterval и SetTimeout можно реализовать асинхронное поведение кода.
Переходим ко второму способу – это использование промисов. Главная проблема асинхронного кода в том, что очень тяжело настроить последовательность выполнения аиснхронных функций. Т.е. вам необходимо, чтобы выполнилась одна асинхронная функция, затем после нее другая. Но асинхронные функции выполняются в не зависимости от друг друга. А как же задать им последовательность? Для этого к нам приходят на помощь промисы.
Промис – это объект, внутрь которого вы засовываете асинхронную функцию, и объект промис будет вам сообщать выполнилась ли эта функция или нет. Т.е. объект промис выступает эдаким индикатором выполнения асинхронной функции. У промиса есть три состояния:
Ожидание (Pending): это состояние когда асинхронная функция находится на стадии выполнения;
Состояние выполнено (Fulfilled): промис переключается на это состояние, когда асинхронная функция завершила выполнение;
Состояние отклонено (Rejected): данное состояние говорит нам о том, что асинхронная функция не смогла выполнится из-за какой-то ошибки.
Давайте же создадим промис. Сперва создадим переменную. Назовем её например, myPromise, в качестве значения этой переменной зададим объект промисес помощью ключевого слова new.
На этом данная инструкция не закончена. Мы должны внутрь круглых скобок поместить асинхронную функцию, за которой промис и будет следить. Давайте реализуем эту функцию с помощью стрелочной функции. У данной асинхронной функции обязательно должны быть два параметра, первый параметр будет сообщать, что асинхронная функция выполнилась успешно, которой параметр будет сообщать об ошибке. Их можно именовать как угодно, но обычно принято их называть resolve и reject
Далее напишем тело функции. Оно будет примитивным. Мы просто переменной isTrue зададим значение true.
Вот такая примитивная асинхронная функция. Далее должна пройти проверка, выполнилась ли эта инструкция или нет. Если выполнилась то мы запустим функцию resolve. Да, я забыл сказать, что эти два параметра, точнее аргументы этих параметров являются функциями. Итак проверяем результат выполнения этой инструкции с помощью if.
Итак если isTrue имеет значение true, то запускаем функцию resolve. А если нет то reject.
Кстати в resolve и rejetc мы можем передать любой тип данных, чтобы им воспользовалась следующая асинхронная функция. Давайте передадим такой текст.
Промис готов, он начнет выполняться когда транслятор дойдет до этой инструкции. Т.е. нам не надо его где-то отдельно вызывать. Далее мы хотим как выполнится эта асинхронная функция, запустить другую асинхронную функцию, или обработать ошибку, если пойдет что-то не так. Это можно сделать с помощью методов then и catch.
Метод then используется, если первая асинхронная функция выполнилась успешно. Метод catch нужен, если что-то пошло не так.
Давайте их используем. Сперва пишем имя переменной, внутри которого находится промис, который сообщит выполнилась ли первая асинхронная функция. Далее пишем метод then, и в скобках мы должны написать следующую асинхронную функцию которую надо выполнить. Давайте снова воспользуемся стрелочной функцией. Если мы напишем параметр, например, обычно его называют result, то сюда передаться значение, которое мы написали в круглых скобках для функции resolve. Обычно этот параметр называют result. И давайте в теле функции выведем этот result. Далее пишем метод catch, где делаем все тоже самое. Только давайте параметр назовем error. Метод catch сработает тогда, когда выполнится функция reject. Т.е. у нас в этой строчке выполнится или метод then или catch, который в свою очередь запустит соответствующую асинхронную функцию.
Запускаем. Проверяем консоль. И все работает.
Вернемся к нашему коду. Смотрите, вот так как мы написали эту строчку, я имею ввиду форматирование, так особо никогда не пишут, потому что такой код трудно читать. И методы then и catch обычно пишут так.
Вот так читать код гораздо удобнее.
Также можно обойтись без параметра catch, так как обработать ошибку, можно во втором параметре стрелочной функции в методе then.
Но так писать не рекомендуют, так как такой код сложнее читать чем с методом catch. Вернем все как было.
Так как функция then является методом промиса, и срабатывает тогда, когда состояние промиса является выполненным, то если мы в внутри этого then вернем еще один промис, то к этому новому промису сможем вызвать его метод then. Пишется это так:
Такой способ записи методов then называется цепочкой промисов, или цепочкой методов then. Метод catch, соответственно, среагирует на ту функцию reject которая и вызовется. Если вызовется этот reject, то в консоль выведется это сообщение.
Также помимо then и catch, у промиса есть еще функция finally, которая сработает в любом случае. И в зависимости от того, в каком порядке вы его укажете, в том порядке он и выполнится. Если мы его напишем в конце, то выполнится он самым последним, если в начале, то самым первым.
А что если у нас есть несколько промисов, для которых мы должны выполнить один и тот же метод then. Давайте напишем три промиса.
Далее помимо методов самого промиса, мы можем пользоваться методами объекта-класса Promise. И первый его статический метод, который мы разберем это all, который позволяет для всех промисов выполнить один и тот же метод then и catch.
Давайте его напишем.
асинхронная функция в методе then получает массив, в нашем случае массив строк в качестве result. А асинхронная функция в методе then как обычно получает один аргумент у сработавшей функции reject. Давайте запустим. И мы получили массив. А теперь сделаем так чтобы у второго промиса сработал reject. И у нас вывелся текст, который мы поместили в функцию reject.
Т.е. если функция rejected была вызвана хотя бы на одном промисе, то сработает только метод catch один раз. Если мы вызовем rejected и в первом промисе, то catch выведет данное сообщение, и остальные вызовы rejected обрабатывать не будет.
Но нас такой результат может не устраивать, может вы хотим, чтобы если один промис вызывает функцию resolve, то он выполнился в любом случае, не смотря на то что другой промис выполнит функцию reject. Для такой цели у объекта-класса Promise есть метод allSetlers, который вернет массив объектов, внутри которых будет информация выполнился ли промис или нет. Здесь нам не нужен метод catch.
И вот мы в консоли получили массив объектов, где в каждом объекте с помощью его ключей мы можем узнать выполнился ли данный промис или нет. И всеми этими ключами вы можете пользоваться в методе then.
Давайте рассмотрим следующий статический метод объекта-класса Promise. Это метод any. Метод any перестанет работать в тот момент, когда успешно выполнится любой из промисов. Давайте сделаем так чтобы и второй промис вызывал функцию resolve. Т.е. сейчас у на два промиса, которые вызывают функцию резолв – это второй и третий промисы.
И вот в результате выполнился только второй промис так как он первым вызвал функцию resolve. Метод catch сработает если все три промиса будут отклонены и передаст в параметр Error ошибку, что ни один из промисов не был выполнен. Т.е. здесь говорится, что сработал специальный для таких случаев объект AggregateError который передает вам сообщение, что все промисы были отменены.
И еще есть один статический метод у объекта-класса промис. Честно говоря на практике я им никогда не пользовался. Это метод race. Он закончит свое выполнение после обработки первого промиса, в независимости какую функцию этот промис вызовет.
И вот как видите в консоли обработался только первый промис. И даже если мы на нем вызовим функцию resolve. Обработается только он.