Привіт усім! Сьогодні ми зануримося у світ асинхронного програмування в JavaScript та TypeScript і створимо власну реалізацію однієї з найкорисніших функцій для роботи з промісами – Promise.all
. Навіщо? Щоб краще зрозуміти, як вона працює! Це як розібрати іграшку, щоб побачити, що всередині.
Що таке Проміс (Promise)? Уявіть собі очікування
Перш ніж ми почнемо, давайте швидко згадаємо, що таке проміс.
Уявіть, що ви замовили піцу. Вам дають чек – це обіцянка (Promise), що піцу доставлять. Ви не знаєте точно, коли вона прибуде, але знаєте, що вона або приїде (успішне виконання, resolve), або щось піде не так (наприклад, піцерія закрилася), і вам зателефонують з поганими новинами (відхилення, reject).
Проміс у програмуванні – це об’єкт, що представляє кінцевий результат асинхронної операції (наприклад, завантаження даних з сервера).
Що робить Promise.all
? Чекаємо на всіх друзів
А тепер уявіть, що ви влаштовуєте вечірку і чекаєте на кількох друзів. Ви хочете почати грати в настільну гру, але тільки тоді, коли всі запрошені прийдуть.
Promise.all
робить щось схоже:
- Він приймає масив (список) промісів.
- Він чекає, поки всі проміси в цьому списку успішно виконаються.
- Якщо всі проміси виконались успішно,
Promise.all
повертає новий проміс, який виконується зі списком результатів усіх промісів (причому в тому ж порядку!). - Але! Якщо хоча б один проміс зі списку зазнає невдачі (reject), то
Promise.all
негайно зазнає невдачі з тією ж причиною, не чекаючи на інші проміси.
Наша мета: Створити myPromiseAll
Ми хочемо написати функцію myPromiseAll
, яка буде поводитися так само, як і стандартний Promise.all
.
Вимоги:
- Приймає масив промісів
promises
. - Повертає новий проміс.
- Цей новий проміс вирішується (resolve) масивом результатів, коли всі вхідні проміси вирішені. Результати мають бути в тому ж порядку, що й вхідні проміси.
- Якщо будь-який вхідний проміс відхиляється (reject), новий проміс негайно відхиляється з тією ж помилкою.
- Якщо передати порожній масив, функція має негайно повернути проміс, що вирішується порожнім масивом.
Крок за кроком до власної реалізації
Давайте напишемо код на TypeScript. Використання TypeScript допоможе нам з типами, роблячи код безпечнішим і зрозумілішим.
/**
* Власна реалізація Promise.all.
* * @param promises Масив промісів (або значень, які не є промісами).
* @returns Новий проміс, який вирішується масивом результатів,
* коли всі вхідні проміси вирішені, або відхиляється,
* якщо будь-який з вхідних промісів відхиляється.
*/
function myPromiseAll<T extends readonly unknown[] | []>(
promises: T
): Promise<{ -readonly [P in keyof T]: Awaited<T[P]> }> {
// Повертаємо новий проміс - це і є результат нашої функції
return new Promise((resolve, reject) => {
// Перетворюємо вхідний ітерований об'єкт (наприклад, масив)
// на справжній масив, щоб з ним було зручно працювати.
const promisesArray = Array.from(promises);
const results: unknown[] = []; // Масив для зберігання результатів
let completedCount = 0; // Лічильник завершених промісів
const totalPromises = promisesArray.length; // Загальна кількість промісів
// ---- Обробка крайнього випадку: порожній масив ----
if (totalPromises === 0) {
// Якщо промісів немає, одразу вирішуємо з порожнім масивом
resolve([] as any); // `as any` тут для спрощення, у складніших типах можна уточнити
return;
}
// ---- Основна логіка: обробка кожного промісу ----
promisesArray.forEach((promise, index) => {
// Обертаємо кожен елемент у Promise.resolve(),
// на випадок, якщо у вхідному масиві є не проміси, а звичайні значення.
Promise.resolve(promise)
.then((result) => {
// ---- Успішне виконання промісу ----
// Зберігаємо результат у правильному місці масиву `results`.
// Це важливо для збереження порядку!
results[index] = result;
// Збільшуємо лічильник завершених промісів
completedCount++;
// Перевіряємо, чи всі проміси вже завершились
if (completedCount === totalPromises) {
// Якщо так, вирішуємо головний проміс з масивом результатів
resolve(results as any); // `as any` тут для спрощення
}
})
.catch((error) => {
// ---- Невдача (відхилення) промісу ----
// Якщо хоча б один проміс зазнав невдачі,
// негайно відхиляємо головний проміс з цією ж помилкою.
reject(error);
});
});
});
}
// ---- Приклад використання ----
const promise1 = Promise.resolve(3);
const promise2 = 42; // Не проміс, але myPromiseAll має це обробити
const promise3 = new Promise<string>((resolve) => {
setTimeout(resolve, 100, 'foo'); // Проміс, що вирішується через 100мс
});
const promise4WithError = Promise.reject("Ой, помилка!");
console.log("Тестуємо з успішними промісами:");
myPromiseAll([promise1, promise2, promise3])
.then(results => {
console.log("Результати:", results); // Очікуємо: [3, 42, "foo"]
})
.catch(error => {
console.error("Не мало статися:", error);
});
console.log("\nТестуємо з промісом, що відхиляється:");
myPromiseAll([promise1, promise4WithError, promise3])
.then(results => {
console.log("Не мало статися:", results);
})
.catch(error => {
console.error("Спіймана помилка:", error); // Очікуємо: "Ой, помилка!"
});
console.log("\nТестуємо з порожнім масивом:");
myPromiseAll([])
.then(results => {
console.log("Результат для порожнього масиву:", results); // Очікуємо: []
})
.catch(error => {
console.error("Не мало статися:", error);
});
Розбір коду: Що тут найважливіше?
- Дженеріки (
<T extends ...>
та Типи Повернення): TypeScript дозволяє нам точно описати, що функція приймає масив (T
) і повертає проміс, який вирішується масивом того ж “розміру”, де кожен елемент є результатом відповідного промісу (Awaited<T[P]>
). Це робить код надійнішим. new Promise((resolve, reject) => { ... })
: Ми створюємо новий проміс, який наша функція поверне. Всередині ми керуємо його станом, викликаючиresolve
абоreject
.results
масив таindex
: Дуже важливо зберігати результати в масивіresults
за тим самим індексом, що й проміс у вхідному масиві. Асинхронні операції можуть завершуватися в будь-якому порядку, алеPromise.all
гарантує порядок результатів.completedCount
: Цей лічильник – ключ до того, щоб зрозуміти, коли всі проміси завершилися успішно. Тільки тоді ми можемо викликатиresolve
головного промісу.Promise.resolve(promise)
: Це дозволяє нашій функції працювати не тільки з промісами, але й зі звичайними значеннями у вхідному масиві. Якщо передати значення, воно просто обертається у вже вирішений проміс..catch(reject)
: Обробка помилки проста: як тільки будь-який проміс відхиляється, ми негайно відхиляємо наш головний проміс.
Висновок
Створення власної версії Promise.all
– це чудовий спосіб зрозуміти, як обробляти кілька асинхронних операцій одночасно, як керувати їхніми результатами та помилками, і чому порядок результатів важливий. Тепер ви не просто користуєтеся Promise.all
, а знаєте, яка магія (і логіка!) відбувається за лаштунками.
Сподіваюся, це пояснення було зрозумілим і корисним! Вдалого кодування!