Асинхронное программирование — это парадигма программирования, которая позволяет вам писать код, который выполняется асинхронно
. В отличие от синхронного программирования, которое выполняет код последовательно, асинхронное программирование позволяет коду выполняться в фоновом режиме, пока остальная часть программы продолжает выполняться. Это особенно полезно для задач, которые могут занять много времени для завершения, таких как получение данных из удаленного API.
Асинхронное программирование
имеет решающее значение для создания отзывчивых и эффективных приложений на JavaScript. TypeScript, надмножество JavaScript, делает работу с асинхронным программированием еще проще.
Существует несколько подходов к асинхронному программированию
в TypeScript, включая использование промисов
, async/await
и колбеков
. Мы подробно рассмотрим каждый из этих подходов, чтобы вы могли выбрать лучший(ие) для вашего случая использования.
Содержание
Почему важно использовать асинхронное программирование?
Асинхронное программирование имеет решающее значение для создания отзывчивых и эффективных веб-приложений. Оно позволяет задачам выполняться в фоновом режиме, а программа продолжает работу, обеспечивая отзывчивость пользовательского интерфейса на ввод. Кроме того, асинхронное программирование может повысить общую производительность, позволяя выполнять несколько задач одновременно.
Существует множество примеров использования асинхронного программирования в реальном мире, таких как доступ к камерам и микрофонам пользователей и обработка событий пользовательского ввода. Даже если вы не часто создаете асинхронные функции, важно знать, как правильно ими пользоваться, чтобы убедиться, что ваше приложение надежно и хорошо работает.
Как TypeScript упрощает асинхронное программирование
TypeScript предлагает несколько функций, которые упрощают асинхронное программирование, включая типобезопасность
, вывод типов
, проверку типов
и аннотации типов
.
С помощью типобезопасности вы можете гарантировать, что ваш код работает ожидаемым образом, даже при работе с асинхронными функциями. Например, TypeScript может обнаруживать ошибки, связанные с значениями null и undefined на этапе компиляции, экономя ваше время и усилия при отладке.
Вывод типов и проверка типов TypeScript также сокращают количество шаблонного кода, который вам нужно писать, делая ваш код более лаконичным и легкочитаемым.
А аннотации типов TypeScript обеспечивают ясность и документирование вашего кода, что особенно полезно при работе с асинхронными функциями, которые могут быть сложными для понимания.
Теперь давайте погрузимся и узнаем о трех ключевых особенностях асинхронного программирования: promises, async/await и callbacks.
Как использовать Promises в TypeScript
Promises – мощный инструмент для обработки асинхронных операций в TypeScript. Например, вы можете использовать promise для получения данных из внешнего API или выполнения длительной задачи в фоновом режиме, пока ваш основной поток продолжает работу.
Для использования Promise создайте новый экземпляр класса Promise
и передайте ему функцию, которая выполняет асинхронную операцию. Эта функция должна вызвать метод resolve с результатом при успешном выполнении операции или метод reject с ошибкой в случае сбоя.
После создания Promise вы можете присоединить к нему обратные вызовы, используя метод then
. Эти обратные вызовы будут вызваны, когда Promise будет выполнен, а разрешенное значение будет передано в качестве параметра. Если Promise будет отклонен, вы сможете присоединить обработчик ошибок, используя метод catch, который будет вызван с причиной отклонения.
Использование Promises предлагает несколько преимуществ по сравнению с традиционными методами на основе обратных вызовов. Например, Promises могут помочь избежать “ада обратных вызовов”, распространенной проблемы в асинхронном коде, где вложенные обратные вызовы становятся трудными для чтения и поддержки.
Кроме того, Promises упрощают обработку ошибок в асинхронном коде, так как вы можете использовать метод catch для управления ошибками, которые возникают в любом месте цепочки Promise.
Наконец, Promises могут упростить ваш код, предоставляя последовательный, композиционный способ обработки асинхронных операций, независимо от их базовой реализации.
Как создать Promise
Синтаксис Promise:
const myPromise = new Promise((resolve, reject) => {
// Выполнить асинхронную операцию
// Если операция успешна, вызовите resolve с результатом
// Если операция завершится неудачно, вызовите reject с объектом ошибки
});
myPromise
.then((result) => {
// Обработка успешного результата
})
.catch((error) => {
// Обработка ошибки
});
// Пример 1 по созданию promise
function myAsyncFunction(): Promise<string> {
return new Promise<string>((resolve, reject) => {
// Некоторая асинхронная операция
setTimeout(() => {
// Успешная операция разрешает promise. Посмотрите мою последнюю статью в блоге о владении асинхронным программированием в TypeScript! Узнайте, как работать с Promises, Async/Await и Callbacks, чтобы писать эффективный и масштабируемый код. Готовьтесь к повышению своего уровня владения TypeScript!
const success = true;
if (success) {
// Разрешить promise с результатом операции, если операция была успешной
resolve(
`The result is success and your operation result is ${operationResult}`
);
} else {
const rejectCode: number = 404;
const rejectMessage: string = `The result is failed and your operation result is ${rejectCode}`;
// Отклонить promise с результатом операции, если операция завершилась неудачно
reject(new Error(rejectMessage));
}
}, 2000);
});
}
// Использовать promise
myAsyncFunction()
.then((result) => {
console.log(result); // вывод: Результат успешен, ваш результат операции - 4
})
.catch((error) => {
console.error(error); // вывод: Результат неудачен, ваш результат операции - 404
});
В приведенном выше примере у нас есть функция под названием myAsyncFunction()
, которая возвращает promise
. Мы используем конструктор Promise
для создания обещания, который принимает callback function
с аргументами resolve
и reject
. Если асинхронная операция успешна, мы вызываем функцию разрешения. Если она не удалась, мы вызываем функцию отклонения.
Объект обещания, возвращаемый конструктором, имеет метод then()
, который принимает функции обратного вызова для успеха и неудачи. Если обещание успешно разрешено, функция обратного вызова успеха вызывается с результатом. Если обещание отклонено, функция обратного вызова неудачи вызывается с сообщением об ошибке.
Объект обещания также имеет метод catch()
, который используется для обработки ошибок, возникающих в цепочке обещаний. Метод catch()
принимает функцию обратного вызова, которая вызывается, если в цепочке обещаний возникает какая-либо ошибка.
Теперь давайте перейдем к тому, как соединять обещания в TypeScript.
Как соединять обещания
Соединение обещаний позволяет выполнять несколько асинхронных операций
по очереди или параллельно. Это полезно, когда вам нужно выполнить несколько асинхронных задач одну за другой или одновременно. Например, вам может понадобиться асинхронно получить данные, а затем асинхронно их обработать.
Давайте рассмотрим пример того, как соединять обещания:
// Пример того, как работает цепочка обещаний
// Первое обещание
const promise1 = new Promise((resolve, reject) => {
const functionOne: string = "This is the first promise function";
setTimeout(() => {
resolve(functionOne);
}, 1000);
});
// Второе обещание
const promise2 = (data: number) => {
const functionTwo: string = "This is the second second promise function";
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(` ${data} '+' ${functionTwo} `);
}, 1000);
});
};
// Сцепление первого и второго обещаний вместе
promise1
.then(promise2)
.then((result) => {
console.log(result); // вывод: Это функция первого обещания + Это функция второго обещания
})
.catch((error) => {
console.error(error);
});
В приведенном выше примере у нас есть два обещания: promise1
и promise2
. promise1
разрешается через 1 секунду со строкой “Это функция первого обещания.” promise2
принимает число как входные данные и возвращает обещание, которое разрешается через 1 секунду со строкой, которая объединяет входное число и строку “Это функция второго обещания.”
Мы цепляем два обещания вместе, используя метод then
. Выход promise1
передается как входные данные для promise2
. Наконец, мы снова используем метод then
, чтобы зарегистрировать вывод promise2
в консоль. Если либо promise1
, либо promise2
отклоняется, ошибка будет перехвачена методом catch
.
Поздравляем! Вы узнали, как создавать и цеплять обещания в TypeScript. Теперь вы можете использовать обещания для выполнения асинхронных операций в TypeScript. Теперь давайте исследуем, как работает Async/Await
в TypeScript.
Как использовать Async/Await в TypeScript
Async/await — это синтаксис, введенный в ES2017, чтобы упростить работу с Promises. Он позволяет писать асинхронный код, который выглядит и ощущается как синхронный код.
В TypeScript вы можете определить асинхронную функцию, используя ключевое слово async
. Это говорит компилятору, что функция асинхронная и вернет Promise.
Теперь давайте посмотрим, как использовать async/await в TypeScript.
Синтаксис Async / Await:
// Синтаксис Async / Await в TypeScript
async function functionName(): Promise<ReturnType> {
try {
const result = await promise;
// код для выполнения после разрешения промиса
return result;
} catch (error) {
// код для выполнения, если промис отклонен
throw error;
}
}
В приведенном выше примере functionName
— это асинхронная функция, которая возвращает Promise типа ReturnType
. Ключевое слово await
используется для ожидания разрешения промиса перед переходом к следующей строке кода.
Блок try/catch
используется для обработки любых ошибок, которые возникают во время выполнения кода внутри асинхронной функции. Если происходит ошибка, она будет поймана блоком catch, где вы сможете обработать ее соответствующим образом.
Использование стрелочных функций с Async / Await
Вы также можете использовать стрелочные функции с синтаксисом async/await в TypeScript:
const functionName = async (): Promise<ReturnType> => {
try {
const result = await promise;
// код для выполнения после разрешения промиса
return result;
} catch (error) {
// код для выполнения, если промис отклонен
throw error;
}
};
В приведенном выше примере functionName
определен как стрелочная функция, которая возвращает Promise типа ReturnType
. Ключевое слово async указывает на то, что это асинхронная функция, а ключевое слово await используется для ожидания выполнения промиса перед переходом к следующей строке кода.
Асинхронные функции / Ожидание с вызовом API
Теперь давайте выйдем за пределы синтаксиса и получим данные из API, используя async/await.
interface User {
id: number;
name: string;
email: string;
}
const fetchApi = async (): Promise<void> => {
try {
const response = await fetch("https://jsonplaceholder.typicode.com/users");
if (!response.ok) {
throw new Error(
`Failed to fetch users (HTTP status code: ${response.status})`
);
}
const data: User[] = await response.json();
console.log(data);
} catch (error) {
console.error(error);
throw error;
}
};
fetchApi();
Здесь мы получаем данные из API JSONPlaceholder, конвертируем их в JSON, а затем выводим в консоль. Это реальный пример того, как использовать async/await в TypeScript.
Вы должны увидеть информацию о пользователе в консоли. Это изображение показывает вывод:
Асинхронные функции / Ожидание с вызовом API Axios
// Пример 2 о том, как использовать async / await в TypeScript
const fetchApi = async (): Promise<void> => {
try {
const response = await axios.get(
"https://jsonplaceholder.typicode.com/users"
);
const data = await response.data;
console.log(data);
} catch (error) {
console.error(error);
}
};
fetchApi();
В приведенном выше примере мы определяем функцию fetchApi()
с использованием async/await и метода Axios.get()
для выполнения HTTP GET запроса к указанному URL. Мы используем await, чтобы дождаться ответа, затем извлекаем данные с помощью свойства data объекта ответа. Наконец, мы выводим данные в консоль с помощью console.log()
. Любые ошибки, которые возникают, перехватываются и выводятся в консоль с помощью console.error()
.
Мы можем достичь этого, используя Axios, так что вы должны увидеть тот же результат в консоли.
Это изображение показывает вывод при использовании Axios в консоли:
Примечание: Прежде чем пробовать приведенный выше код, вам необходимо установить Axios с помощью npm или yarn.
npm install axios
yarn add axios
Если вы не знакомы с Axios, вы можете узнать больше об этом здесь.
Вы можете заметить, что мы использовали блоки try
и catch
для обработки ошибок. Блоки try
и catch
являются методом управления ошибками в TypeScript. Поэтому, когда вы делаете API вызовы, как мы только что сделали, обязательно используйте блоки try
и catch
для обработки любых ошибок.
Теперь давайте рассмотрим более продвинутое использование блоков try
и catch
в TypeScript:
// Пример 3 о том, как использовать async / await в TypeScript
interface Recipe {
id: number;
name: string;
ingredients: string[];
instructions: string[];
prepTimeMinutes: number;
cookTimeMinutes: number;
servings: number;
difficulty: string;
cuisine: string;
caloriesPerServing: number;
tags: string[];
userId: number;
image: string;
rating: number;
reviewCount: number;
mealType: string[];
}
const fetchRecipes = async (): Promise<Recipe[] | string> => {
const api = "https://dummyjson.com/recipes";
try {
const response = await fetch(api);
if (!response.ok) {
throw new Error(`Failed to fetch recipes: ${response.statusText}`);
}
const { recipes } = await response.json();
return recipes; // Возвращаем массив рецептов
} catch (error) {
console.error("Error fetching recipes:", error);
if (error instanceof Error) {
return error.message;
}
return "An unknown error occurred.";
}
};
// Получаем и записываем рецепты в лог
fetchRecipes().then((data) => {
if (Array.isArray(data)) {
console.log("Recipes fetched successfully:", data);
} else {
console.error("Error message:", data);
}
});
В приведенном выше примере мы определяем интерфейс Recipe
, который описывает структуру данных, которую мы ожидаем от API. Затем мы создаем функцию fetchRecipes()
с использованием async/await и метода fetch(), чтобы сделать HTTP GET запрос к указанной конечной точке API.
Мы используем блок try/catch
для обработки любых ошибок, которые могут возникнуть во время запроса к API. Если запрос успешен, мы извлекаем свойство данных из ответа, используя await, и возвращаем его. Если произошла ошибка, мы проверяем наличие сообщения об ошибке и возвращаем его в виде строки, если оно существует.
Наконец, мы вызываем функцию fetchRecipes()
и используем .then()
для записи возвращенных данных в консоль. Этот пример демонстрирует, как использовать async/await
с блоками try/catch
для обработки ошибок в более продвинутом сценарии, где нам нужно извлечь данные из объекта ответа и вернуть пользовательское сообщение об ошибке.
Это изображение показывает результат выполнения кода:
Async / Await с Promise.all
Promise.all()
– это метод, который принимает массив обещаний в качестве входных данных (перечисляемый объект) и возвращает одно обещание в качестве результата. Это обещание разрешается, когда все входные обещания разрешены, или если входной перечисляемый объект не содержит обещаний. Оно немедленно отклоняется, если хотя бы одно из входных обещаний отклонено или если не-обещания вызывают ошибку, и оно будет отклонено с первым сообщением об отклонении или ошибкой.
// Пример использования async / await с Promise.all
interface User {
id: number;
name: string;
email: string;
profilePicture: string;
}
interface Post {
id: number;
title: string;
body: string;
}
interface Comment {
id: number;
postId: number;
name: string;
email: string;
body: string;
}
const fetchApi = async <T>(url: string): Promise<T> => {
try {
const response = await fetch(url);
if (response.ok) {
const data = await response.json();
return data;
} else {
throw new Error(`Network response was not ok for ${url}`);
}
} catch (error) {
console.error(error);
throw new Error(`Error fetching data from ${url}`);
}
};
const fetchAllApis = async (): Promise<[User[], Post[], Comment[]]> => {
try {
const [users, posts, comments] = await Promise.all([
fetchApi<User[]>("https://jsonplaceholder.typicode.com/users"),
fetchApi<Post[]>("https://jsonplaceholder.typicode.com/posts"),
fetchApi<Comment[]>("https://jsonplaceholder.typicode.com/comments"),
]);
return [users, posts, comments];
} catch (error) {
console.error(error);
throw new Error("Error fetching data from one or more APIs");
}
};
fetchAllApis()
.then(([users, posts, comments]) => {
console.log("Users: ", users);
console.log("Posts: ", posts);
console.log("Comments: ", comments);
})
.catch((error) => console.error(error));
В приведенном выше коде мы использовали Promise.all
для одновременного получения данных из нескольких API. Если у вас есть несколько API для получения, вы можете использовать Promise.all
, чтобы получить их все сразу. Как видите, мы использовали map
для перебора массива API, а затем передали его в Promise.all
для одновременного получения.
На изображении ниже показан результат вызовов API:
Давайте посмотрим, как использовать Promise.all
с Axios:
// Пример использования async / await с axios и Promise.all
const fetchApi = async () => {
try {
const urls = [
"https://jsonplaceholder.typicode.com/users",
"https://jsonplaceholder.typicode.com/posts",
];
const responses = await Promise.all(urls.map((url) => axios.get(url)));
const data = await Promise.all(responses.map((response) => response.data));
console.log(data);
} catch (error) {
console.error(error);
}
};
fetchApi();
В приведенном выше примере мы используем Promise.all
для одновременного получения данных с двух разных URL. Сначала мы создаем массив URL, затем используем метод map для создания массива промисов из вызовов axios.get
. Мы передаем этот массив в Promise.all
, который возвращает массив ответов. Наконец, мы снова используем метод map, чтобы получить данные из каждого ответа и вывести их в консоль.
Как использовать колбэки в TypeScript
Колбэк callback — это функция, переданная в качестве аргумента другой функции. Колбэк выполняется внутри другой функции. Колбэки гарантируют, что функция не будет выполняться до завершения задачи, но затем она будет выполнена сразу после завершения задачи. Они помогают нам писать асинхронный код на JavaScript и предотвращают проблемы и ошибки.
// Пример использования колбэков в TypeScript
const add = (a: number, b: number, callback: (result: number) => void) => {
const result = a + b;
callback(result);
};
add(10, 20, (result) => {
console.log(result);
});
На изображении ниже показана функция колбэка:
Давайте посмотрим на другой пример использования колбэков в TypeScript:
// Пример использования функции колбэка в TypeScript
type User = {
name: string;
email: string;
};
const fetchUserData = (
id: number,
callback: (error: Error | null, user: User | null) => void
) => {
const api = `https://jsonplaceholder.typicode.com/users/${id}`;
fetch(api)
.then((response) => {
if (response.ok) {
return response.json();
} else {
throw new Error("Network response was not ok.");
}
})
.then((data) => {
const user: User = {
name: data.name,
email: data.email,
};
callback(null, user);
})
.catch((error) => {
callback(error, null);
});
};
// Использование fetchUserData с функцией колбэка
fetchUserData(1, (error, user) => {
if (error) {
console.error(error);
} else {
console.log(user);
}
});
В приведенном выше примере у нас есть функция под названием fetchUserData
, которая принимает id
и callback
в качестве параметров. Этот callback
— это функция с двумя параметрами: ошибкой и пользователем.
Функция fetchUserData
извлекает данные пользователя из конечной точки API JSONPlaceholder, используя id
. Если запрос выполнен успешно, создается объект User
и передается в функцию обратного вызова с ошибкой null. Если происходит ошибка во время запроса, ошибка отправляется в функцию обратного вызова с пользователем null.
Чтобы использовать функцию fetchUserData
с обратным вызовом, мы передаем аргументы id
и функцию обратного вызова. Функция обратного вызова проверяет ошибки и регистрирует данные пользователя, если ошибок нет.
На изображении ниже показан результат вызовов API:
Как использовать обратные вызовы ответственно
Хотя обратные вызовы являются фундаментальными для асинхронного программирования в TypeScript, их необходимо тщательно управлять, чтобы избежать “ада обратных вызовов” – кода с пирамидальной структурой, глубоко вложенного, который становится трудным для чтения и поддержки. Вот как использовать обратные вызовы эффективно:
-
Избегайте глубокого вложения
-
Упрощайте структуру кода, разбивая сложные операции на именованные функции
-
Используйте обещания (promises) или async/await для сложных асинхронных рабочих процессов (подробнее об этом ниже)
-
-
Обработка ошибок в первую очередь
-
Всегда следуйте соглашению Node.js о параметрах
(ошибка, результат)
-
Проверяйте наличие ошибок на каждом уровне вложенных колбеков
-
function processData(input: string, callback: (err: Error | null, result?: string) => void) {
// ... всегда вызывайте колбек с ошибкой в первую очередь
}
-
Используйте аннотации типов
-
Используйте типовую систему TypeScript для обеспечения сигнатур колбеков
-
Определите четкие интерфейсы для параметров колбеков
-
type ApiCallback = (error: Error | null, data?: ApiResponse) => void;
-
Рассмотрите библиотеки управления потоком
Для сложных асинхронных операций используйте утилиты, такие какasync.js
, для:-
Параллельного выполнения
-
Последовательного выполнения
-
Обработки ошибок в конвейерах
-
Когда использовать обратные вызовы по сравнению с альтернативами
Иногда обратные вызовы являются отличным выбором, а иногда нет.
Обратные вызовы полезны, когда вы работаете с асинхронными операциями (однократное завершение), взаимодействуете с более старыми библиотеками или API, которые ожидают обратные вызовы, обрабатываете слушателей событий (например, слушатели кликов или события веб-сокетов) или создаете легковесные утилиты с простыми асинхронными потребностями.
В других сценариях, когда вам нужно сосредоточиться на написании поддерживаемого кода с четким асинхронным потоком, обратные вызовы могут вызывать проблемы, и вам следует предпочитать промисы или async-await. Например, когда вам нужно объединить несколько операций, обрабатывать сложную передачу ошибок, работать с современными API (например, с API Fetch или FS Promises) или использовать promise.all()
для параллельного выполнения.
Пример миграции от обратных вызовов к промисам:
// Версия с коллбэком
function fetchUser(id: number, callback: (err: Error | null, user?: User) => void) {
// ...
}
// Версия с промисами
async function fetchUserAsync(id: number): Promise<User> {
// ...
}
// Использование с async/await
try {
const user = await fetchUserAsync(1);
} catch (error) {
// Обработка ошибок
}
Эволюция асинхронных паттернов
Паттерн | Плюсы | Минусы |
Коллбэки | Простота, универсальность | Вложенная сложность |
Промисы | Цепляемость, лучший поток ошибок | Требует цепочек .then() |
Async/Await | Читаемость, похожая на синхронную | Требует транспиляции |
Современные проекты на TypeScript часто используют смесь: коллбэки для событийных паттернов и промисы/async-await для сложной асинхронной логики. Ключевым моментом является выбор правильного инструмента для вашей конкретной задачи, сохраняя при этом ясность кода.
Заключение
В этой статье мы узнали о различных способах обработки асинхронного кода в TypeScript. Мы узнали о коллбэках, промисах, async/await и о том, как их использовать в TypeScript. Мы также узнали об этой концепции.
Если вы хотите узнать больше о программировании и о том, как стать лучшим разработчиком программного обеспечения, вы можете подписаться на мой канал на YouTube CliffTech.
Спасибо, что прочитали мою статью. Надеюсь, она вам понравилась. Если у вас есть вопросы, не стесняйтесь обращаться ко мне.
Свяжитесь со мной в социальных сетях: