Что такое Promise в JavaScript
2 года назад·6 мин. на чтение
Обещания (Promises) — это мощная возможность JavaScript, которая упрощает понимание асинхронных операций и улучшает читаемость кода.
Современная веб-разработка в значительной степени опирается на асинхронное программирование, что позволяет нам эффективно обрабатывать трудоемкие операции, не откладывая выполнение других задач. Цель этой статьи — дать полное представление о промисах JavaScript, включая информацию об их определении, этапах жизненного цикла, вспомогательных функциях и внутренней работе.
Когда все объекты Promise во входном массиве выполнены, эта функция возвращает новый объект Promise.
Вот пример:
Как только какой-либо из объектов Promise во входном массиве выполнится, эта функция возвращает новый объект Promise, который либо выполняется, либо отклоняется.
Вот пример:
Без необходимости дополнительных асинхронных операций эти функции позволяют создавать Promise, которые уже были выполнены или отклонены соответственно.
Благодаря мощным возможностям, которые эти вспомогательные функции предлагают для манипулирования и управления Promises, асинхронное программирование JavaScript теперь более адаптируемо и выразительно.
Что такое Promises в JavaScript?
Успех (или сбой) асинхронной операции и значение, которое она генерирует, представлены объектом, называемым обещанием. Вместо вложения обратных вызовов он позволяет нам обрабатывать асинхронный код более элегантным и организованным способом с помощью цепочек методов. Обещания могут находиться в одном из трех состояний: pending (ожидание), fulfilled (выполнен) или rejected (отклонен). Pending - начальное состояние Promise, которое означает, что асинхронная операция все еще выполняется. Если операция выполнена успешно, Promise переходит в состояние “выполнен”, а при возникновении проблемы — в состояние “отклонен”.Этапы жизненного цикла Promise
Давайте подробно рассмотрим каждый этап жизненного цикла Promise на примерах кода:Ожидание
Объект Promise находится в состоянии ожидания на момент создания. На данный момент асинхронная операция все еще продолжается, и обещание не принимается и не отклоняется. Вот пример:const promise = new Promise((resolve, reject) => { // Асинхронная операция, например, запрос данных через API, // resolve(result) или reject(error) будут вызваны позднее });
Выполнено (Fulfilled)
Обещание переходит в состояние “выполнено”, как только асинхронная операция успешно завершена. На этом этапе связанное значение (результат) становится доступным. Для обработки выполненного обещания мы используем метод.then()
Вот пример:
const promise = new Promise((resolve, reject) => { // Симуляция асинхронной операции setTimeout(() => { resolve("Operation succeeded!"); }, 2000); }); promise.then((result) => { console.log(result); // Вывод: "Operation succeeded!" });
Отклонено (Rejected)
В случае, если возникает проблема с асинхронной операцией, Promise переходит в отклоненное состояние. Оно обозначает, что операция не удалась, и предоставляет объекту ошибки соответствующую информацию. Для обработки отклоненного Promise мы используем метод.catch()
Вот пример:
const promise = new Promise((resolve, reject) => { // Симуляция асинхронной операции setTimeout(() => { reject(new Error("Something went wrong!")); }, 2000); }); promise.catch((error) => { console.log(error.message); // Вывод: "Something went wrong!" });
Цепочка обещаний
Обещания имеют ряд важных преимуществ, в том числе возможность объединять несколько асинхронных операций, что улучшает читаемость кода. Мы достигаем этого, используя метод.then()
для возврата нового Promise.
Вот пример:
const getUser = () => { return new Promise((resolve, reject) => { // Симуляция асинхронной операции setTimeout(() => { resolve({ id: 1, name: "John" }); }, 2000); }); }; const getUserPosts = (user) => { return new Promise((resolve, reject) => { // Симуляция асинхронной операции setTimeout(() => { resolve(["Post 1", "Post 2"]); }, 2000); }); }; getUser() .then((user) => getUserPosts(user)) .then((posts) => console.log(posts)); // Вывод: ["Post 1", "Post 2"]
Вспомогательные функции для объектов Promise
Наряду с основными методами, предоставляемыми Promise, JavaScript также предоставляет ряд вспомогательных функций, которые улучшают функциональность и удобочитаемость асинхронного кода. Общие задачи упрощаются благодаря этим вспомогательным функциям, которые также улучшают поток управления и обработку ошибок.Promise.all()
Когда все объекты Promise во входном массиве выполнены, эта функция возвращает новый объект Promise.
Вот пример:
Три функцииconst fetchUser = () => { return new Promise((resolve, reject) => { // Симуляция асинхронного вызова API setTimeout(() => { const user = { id: 1, name: "John" }; resolve(user); }, 2000); }); }; const fetchPosts = () => { return new Promise((resolve, reject) => { // Симуляция асинхронного вызова API setTimeout(() => { const posts = ["Post 1", "Post 2"]; resolve(posts); }, 1500); }); }; const fetchComments = () => { return new Promise((resolve, reject) => { // Симуляция асинхронного вызова API setTimeout(() => { const comments = ["Comment 1", "Comment 2"]; resolve(comments); }, 1000); }); }; Promise.all([fetchUser(), fetchPosts(), fetchComments()]) .then(([user, posts, comments]) => { console.log("User:", user); console.log("Posts:", posts); console.log("Comments:", comments); }) .catch((error) => { console.log("Error:", error); });
fetchUser()
, fetchPosts()
и fetchComments()
включены в приведенный выше пример. Для пользовательских данных, пользовательских сообщений и комментариев пользователей каждая функция имитирует асинхронный вызов API, возвращая Promise.
Передавая массив Promise ([fetchUser(), fetchPosts(), fetchComments()]
) в Promise.all()
, мы создаем новый Promise, который выполняется после успешного выполнения каждого Promise в массиве. При обработке выполнения метод .then()
применяет синтаксис, деструктурирующий массив, для получения разрешенных значений каждого объекта Promise.
Когда в этой ситуации успешно выполняются все обещания, деструктурирование массива присваивает значения fetchUser()
, fetchPosts()
и fetchComments()
переменным user
, posts
и comments
соответственно. Пользователь, публикации и комментарии выводятся в консоль.
Если какой-либо из Promises не удался вызывается .catch()
и ошибка выводится в консоль.
Promise.all()
позволяет нам эффективно извлекать несколько асинхронных ресурсов и обрабатывать их все одновременно после успешного завершения каждого запроса.
Promise.race()
Как только какой-либо из объектов Promise во входном массиве выполнится, эта функция возвращает новый объект Promise, который либо выполняется, либо отклоняется.
Вот пример:
В приведенном выше примере есть три функции, называемыеconst fetchResource = (resource, delay) => { return new Promise((resolve, reject) => { // Симуляция асинхронного вызова API setTimeout(() => { resolve(`${resource} is fetched successfully in ${delay}ms`); }, delay); }); }; const resource1 = fetchResource("Resource 1", 2000); const resource2 = fetchResource("Resource 2", 1500); const resource3 = fetchResource("Resource 3", 1000); Promise.race([resource1, resource2, resource3]) .then((result) => { console.log(result); }) .catch((error) => { console.log(error); });
fetchResource()
которые имитируют асинхронные вызовы API, возвращая Promises. Каждый вызов имитирует время, необходимое для получения определенного ресурса, с разным временем задержки.
Когда массив Promises ([resource1, resource2, resource3]
) передается методу Promise.race(), создается новый Promise, который выполняется (выполняется успешно или отклоняется) в ответ на любое Promise в массиве Promise.race()
. Значение успешного Promise передается в качестве параметра result
и выводится в консоли в методе .then()
, который используется для обработки выполнения. В этом случае победителем будет считаться тот ресурс, который разрешится первым (т.е. тот, у которого наименьшая задержка), а его значение будет выведено на консоль.
Если какой-либо из Promises не удался, то вызывается .catch()
и ошибка выводится в консоли.
Мы можем выполнить несколько асинхронных операций одновременно и отреагировать на результат самой быстрой с помощью метода Promise.race()
. Это полезно в ситуациях, когда мы хотим действовать в соответствии с первоначальным ответом или завершением.
Promise.resolve()
и Promise.reject()
Без необходимости дополнительных асинхронных операций эти функции позволяют создавать Promise, которые уже были выполнены или отклонены соответственно.
Благодаря мощным возможностям, которые эти вспомогательные функции предлагают для манипулирования и управления Promises, асинхронное программирование JavaScript теперь более адаптируемо и выразительно.
Promise.resolve()
и Promise.reject()
используются в следующем примере кода:
Функцияconst fetchData = (shouldSucceed) => { if (shouldSucceed) { return Promise.resolve("Data fetched successfully"); } else { return Promise.reject(new Error("Failed to fetch data")); } }; fetchData(true) .then((result) => { console.log(result); }) .catch((error) => { console.log(error); }); fetchData(false) .then((result) => { console.log(result); }) .catch((error) => { console.log(error); });
fetchData()
в приведенном выше примере имеет параметр shouldSucceed
, который указывает, должна ли выборка данных быть успешной или неудачной. Promise.resolve()
используется для создания и возврата объекта Promise, который немедленно выполняется с сообщением «Data fetched successfully», если shouldSucceed
имеет значение true
.
Promise.reject()
используется для создания и возврата объекта Promise, который немедленно отклоняется с новым объектом Error
и сообщением «Failed to fetch data», если shouldSucceed
имеет значение false
.
Возвращенное обещание выполняется в первом вызове fetchData()
с shouldSucceed
, равным true
, и выполнение управляется методом .then()
. В result
передается значение «Data fetched successfully», которое затем выводится в консоль.
Во втором вызове fetchData()
с shouldSucceed
, равным false
, возвращенный объект Promise отклоняется, и для обработки отклонения используется .catch()
. Объект ошибки, содержащий сообщение «Failed to fetch data», передается в качестве параметра error
и выводится в консоль.
Используя Promise.resolve()
и Promise.reject()
мы можем легко создавать Promise
, которые уже разрешены или отклонены, без необходимости дополнительных асинхронных операций. Это полезно при обработке синхронных значений или ошибок в виде объектов Promise.
Итоги
Асинхронное программирование JavaScript произвело революцию благодаря обещаниям, которые предлагают хорошо организованный и красивый способ решения трудоемких задач. Обещания помогают нам создать более удобочитаемый и поддерживаемый код. Определение объектов Promise, этапы их жизненного цикла и вспомогательные функции, таких какPromise.all()
, Promise.race()
, Promise.resolve()
и Promise.reject()
были рассмотрены в этом обширном руководстве. Мы можем эффективно обрабатывать асинхронные операции и изящно обрабатывать сценарии успеха и ошибок, понимая этапы жизненного цикла - pending
, fulfilled
и rejected
.
Разработчики могут создавать надежные и эффективные приложения, которые легко справляются со сложными асинхронными задачами, используя Promises и их вспомогательные функции. Поток управления оптимизирован, а удобочитаемость кода улучшена за счет цепочек Promise. Кроме того, вспомогательные функции JavaScript улучшают обработку ошибок и упрощают выполнение многочисленных асинхронных операций.
Современная веб-разработка требует использования асинхронного программирования, а знание обещаний позволяет разработчикам создавать код, который является более чистым и простым в обслуживании. Вы можете создавать надежные приложения, которые эффективно обрабатывают асинхронные операции, интегрируя Promises в свои JavaScript проекты и используя их вспомогательные функции.Что такое функтор? Функциональное программирование
2 года назад·5 мин. на чтение
В этой статье на простых и доступных примерах рассмотрим одну из концепций функционального программирования - Функтор.
Это серия статей о функциональном программировании:
Функтор
Как мы обсуждали ранее, нам нужна только обертка, которая абстрагируется от обработки данных. Итак, роль функтора Имплементация функтора
Как использовать функтор
С валидными данными:
Функтор Решение задачи с функтором
Комментарии к решению с функтором
Мы смогли улучшить традиционное решение при помощи функтора
- Парадигмы программирования
- Композиция
- Функторы (рассматривается в этой статье)
- Каррирование
- Чистые функции
- Функции первого класса
Что такое функтор?
Функтор (functor) это:- обертка над значением,
- предоставляет интерфейс для преобразование (map),
- подчиняется законам функтора (поговорим о них позже).
Примеры функторов
- Массив (
Array
), - Промис (
Promise
).
Почему массив - функтор?
Вспомним определение функтора:- обертка над списком значений,
- предоставляет интерфейс для преобразования - метод
map
, - подчиняется законам функтора.
[1, 2, 3] // обернутое значение .map( // интерфейс для преобразования значения x => x * 2 )
Почему промис - функтор?
Промис это:- обертка над любым значением из JavaSctipt типов,
- предоставляет интерфейс для преобразования - метод
then
, - подчиняется законам функтора.
const promise = new Promise((resolve, reject) => { resolve( { data: "value" } // обернутое значение, в данном случае объект ) }); promise .then( // интерфейс для преобразования значения response => console.log(response) );
Что объединяет массив (или промис) и функтор?
Функтор - это паттерн проектирования, аArray
и Promise
- типы данных, которые основаны на этом паттерне.
Почему мы говорим, что массив и промис - функторы?
Чтобы понять, что функторы ближе чем кажутся. Массив и промис легко понять, при это они являются мощной концепцией. Мы используем их ежедневно, даже не подозревая об их сущности.Где использовать функторы?
Немного поговорив о функторах и связав их с нашим повседневным использованием, было бы разумно рассмотреть их подробнее. Чтобы лучше понять идею функтора, создадим свои собственные функторы. Для начала рассмотрим такую задачу. Предположим, есть следующий кусочек данных.{ products: [ { name: "All about JavaScript", type: "book", price: 22, discount: 20 } ] }
Постановка задачи
Найти финальную цену первого товара с учетом скидки. Если по какой-либо причине будут переданы неправильные данные, вывести строку "No data".Шаги алгоритма
- Найти первый продукт со скидкой,
- Применить скидку,
- Продолжать проверку данных на валидность. Если данные не валидны - вернуть "No data".
Традиционное решение
const isProductWithDiscount = product => { return !isNaN(product.discount) } const findFirstDiscounted = products => { products.find(isProductWithDiscount) } const calcPriceAfterDiscount = product => { return product.price - product.discount } const findFinalPrice = (data, fallbackValue) => { if(!data || !data.products) return fallbackValue const discountedProduct = findFirstDiscounted(data.products) if(!discountedProduct) return fallbackValue return calcPriceAfterDiscount(discountedProduct) } findFinalPrice(data, "No data")
Комментарии к традиционному решению
Достоинства:- Атомарные логические единицы (
isProductWithDiscount
,findFirstDiscounted
иcalcPriceAfterDiscount
), - Логику защищена от невалидных данных.
- Cлишком много защитных проверок. (Защитное программирование (Defensive programming) является обязательным в любом отказоустойчивом программном обеспечении. Однако, в нашем коде 50% тела функции
findFinalPrice
— проверка на валидность данных. Это слишком много). fallbackValue
почти везде.
Почему нас волнуют эти улучшения?
Потому что данный код заставляет слишком в него вникать. Это негативно влияет на DX (Developer Experience) - уровень удовлетворенности разработчика от работы с кодом. Проанализируем код, чтобы прийти к лучшему решению. Части, которые мы стремимся улучшить, формируют паттерны (защита (defence) и откат (fallback)). Хорошо то, что эти части на самом деле цельные и атомарные. Мы должны иметь возможность абстрагировать этот паттерн в оболочку, которая могла бы обрабатывать эти крайние случаи вместо нас. Обертка позаботится о крайних случаях, а нам останется позаботиться только о бизнес-логике.Функтор Maybe
Как мы обсуждали ранее, нам нужна только обертка, которая абстрагируется от обработки данных. Итак, роль функтора Maybe
состоит в том, чтобы обернуть наши данные (потенциально невалидные данные) и обработать для нас крайние случаи.
Имплементация функтора Maybe
function Maybe(value) { const isNothing = () => { return value === null || value === undefined } const map = (fn) => { return isNothing() ? Maybe() : Maybe(fn(value)) } const getValueOrFallback = { return (fallbackValue) => isNothing() ? fallbackValue : value; } return { map, getValueOrFallback, }; }
Пояснения к имплементации
isNothing
проверяет валидно ли обернутое в функторMaybe
значениеmap
- интерфейс для преобразования обернутого значения, с помощью которого мы применяем функции с бизнес логикой к обернутому значению.map
возвращает новое значение в другом экземпляреMaybe
. Таким образом, мы можем сделать цепочку вызововmap
-.map().map().map...
.getValueOrFallback
возвращает обернутое значение или запасное значениеfallbackValue
.
Как использовать функтор Maybe
?
С валидными данными:
С невалидными данными:Maybe('Hello') .map(x => x.substring(1)) .getValueOrFallback('fallback') // 'ello'
Maybe(null) .map(x => x.substring(1)) // функция не будет запущена .getValueOrFallback('fallback') // 'fallback'
Maybe
обработал крайние случаи вместо нас и не запустил функцию с невалидными данными. Нам нужно лишь позаботиться о бизнес логике. Таким образом, мы внедрили улучшение, о котором говорили в традиционном решении. Внедрим это решение в задачу.
Решение задачи с функтором Maybe
const isProductWithDiscount = product => { return !isNaN(product.discount) } const findFirstDiscounted = products => { return products.find(isProductWithDiscount) } const calcPriceAfterDiscount = product => { return product.price - product.discount } Maybe(data) .map((x) => x.products) .map(findFirstDiscounted) .map(calcPriceAfterDiscount) .getValueOrFallback("No data")
Комментарии к решению с функтором Maybe
Мы смогли улучшить традиционное решение при помощи функтора Maybe
:
- мы не защищаем код сами, вместо нас это делает функтор
Maybe
, - мы указали
fallbackValue
только один раз.
Maybe
соответствует определению функтора? Функтор Maybe
это:
- обертка над любым значением из JavaScript типов,
- предоставляет интерфейс для преобразования - метод
map
, - подчиняется законам функтора.
Законы функторов
Закон идентичности (Identity law)
Если при выполнении операции преобразования, значения в функторе преобразовываются сами на себя, результатом будет немодифицированный функтор.const m1 = Maybe(value) const m2 = Maybe(value).map(v => v) // m1 и m2 эквивалентны
Закон композиции (Composition law)
Если две последовательные операции преобразования выполняются одна за другой с использованием двух функций, результат должен быть таким же, как и при одной операции отображения с одной функцией, что эквивалентно применению первой функции к результату второй.const m1 = Maybe(value).map(v => f(g(v))) const m2 = Maybe(value).map(v => g(v)).map(v => f(v)) // m1 и m2 эквивалентны
Зачем использовать функторы?
- Абстракция над применением функции,
- Усиление композиции функций,
- Уменьшение количества защитного кода (как в функторе
Maybe
), - Более чистая структура кода,
- Переменные более явно указывают на то, что мы ожидаем (что
Maybe
моделирует значение, которое может присутствовать, а может и не присутствовать).
Что означает Абстракция над применением функции?
То, что мы передаем функцию (т.е.x => x.products
) в интерфейс преобразования (т.е. map
) обертки (т.е. Maybe
), и она знает, как позаботиться о себе (посредством своей внутренней реализации).
Нас не интересуют детали реализации оболочки, которые она содержит (детали реализации скрыты), и тем не менее мы знаем, как использовать обертку (Array
или Promise
), используя их интерфейсы преобразования (map
).
И это на самом деле крайне важно в мире программирования. Это снижает планку того, как много мы, как программисты, должны понимать, чтобы иметь возможность что-то сделать. Функторы могут быть реализованы на любом языке, поддерживающем функции высшего порядка (а таких в наши дни большинство).
Почему функторы не используются повсеместно?
Просто потому, что мы к ним не привыкли. До.map
(и .then
) мы мутировали массивы или перебирали их значения вручную. Но как только мы обнаружили .map
, мы начали адаптировать его в качестве нового инструмента преобразования. Я надеюсь, что, поняв ценность функторов, мы начнем чаще внедрять их в наши ежедневные задачи как привычный инструмент.
Функтор Maybe
- лишь пример функтора. Существует множество функторов, которые выполняет различные задачи. В этой статье мы рассмотрели самый простой из них, чтобы понять саму идею функторов.