Пропозиції (Promises) в JavaScript є потужним інструментом для управління асинхронними операціями, але іноді їх розуміння може бути ускладнене через те, як вони працюють "під капотом". Давайте розглянемо деякі основні концепти і приклади, щоб прояснити їхню поведінку.
Основи Promises
Promise — це об'єкт, який представляє майбутнє завершення (або невдачу) асинхронної операції та її результатуюче значення. Пропозиції можуть бути в одному з трьох станів:
- Pending (Очікування): Операція ще не завершена.
- Fulfilled (Виконано): Операція успішно завершена, і значення доступне.
- Rejected (Відхилено): Операція не вдалася, і є причина (помилка).
Коли Promise створюється за допомогою конструктора Promise
, функція виконавця (executor function) виконується синхронно. Це може бути несподіванкою, оскільки пропозиції зазвичай асоціюються з асинхронною поведінкою.
Створення Promise: Функція виконавця
Ось приклад:
const promise = new Promise(function executorFunction(resolve, reject) {
console.log("Функція виконавця викликана конструктором Promise");
// Симулюємо довгу, блокуючу операцію
for (let i = 0; i < 10000000000; i++) {}
});
console.log("Об'єкт Promise створено");
console.log(promise);
Що відбувається:
- Функція виконавця всередині конструктора
Promise
виконується синхронно. - Спочатку виконується
console.log("Функція виконавця викликана конструктором Promise")
. - Блокуючий код (цикл
for
) виконується до кінця, затримуючи виконання наступних рядків коду. - Після завершення циклу виконуються
console.log("Об'єкт Promise створено")
іconsole.log(promise)
.
Виведення:
Функція виконавця викликана конструктором Promise
[БЛОКУЮЧИЙ КОД ВИКОНУЄТЬСЯ...]
Об'єкт Promise створено
Promise { }
Promise залишатиметься в стані pending (Очікування), оскільки ні resolve
, ні reject
не були викликані.
Уникання блокуючого коду
Одна з основних переваг пропозицій — це можливість обробляти асинхронні операції без блокування циклу подій. Включення блокуючих операцій, таких як цикл for
, порушує це правило.
Для симуляції затримок без блокування використовуйте setTimeout
:
const promise = new Promise(function executorFunction(resolve, reject) {
console.log("Функція виконавця викликана конструктором Promise");
setTimeout(() => {
resolve("Задача завершена");
}, 2000); // Симулюємо затримку на 2 секунди
});
console.log("Об'єкт Promise створено");
console.log(promise);
promise.then((result) => {
console.log("Promise вирішено з результатом:", result);
});
Виведення:
Функція виконавця викликана конструктором Promise
Об'єкт Promise створено
Promise { }
Promise вирішено з результатом: Задача завершена
У цьому випадку Promise виконується через 2 секунди без блокування інших операцій.
Ключові ідеї
- Функція виконавця є синхронною:
- Функція виконавця виконується негайно при створенні Promise. Важливо уникати довготривалих задач тут.
2. Асинхронні операції:
- Завжди надавайте перевагу асинхронним операціям всередині Promise для повного використання неблокуючої природи JavaScript.
3. Стані Promise:
- Promise може змінювати свій стан з pending (Очікування) на fulfilled (Виконано) або rejected (Відхилено), і цей перехід відбувається лише один раз.
Черга мікротасків і поведінка Promise
Promise внутрішньо керують зворотними викликами onFulfillment та onRejection через чергу мікротасків.
Це гарантує, що обробники виконання чи відхилення Promise будуть виконуватись якнайшвидше після завершення поточного контексту виконання JavaScript.
Ключові деталі:
- onFulfillment (виконано) та onRejection (відхилено) — це внутрішньо керовані масиви, в яких зберігаються функції зворотного виклику.
- Спочатку ці масиви порожні, і функції зворотного виклику можуть бути додані за допомогою
.then()
для onFulfillment або.catch()
для onRejection. - Якщо Promise переходить у стан виконано (fulfilled), всі функції зворотного виклику, що зберігаються в масиві onFulfillment, додаються в чергу мікротасків (microtasks queue), а масив onRejection ігнорується.
- Якщо Promise переходить у стан відхилення (rejected), всі функції зворотного виклику, що зберігаються в масиві onRejection, додаються в чергу мікротасків, а масив onFulfillment ігнорується.
Приклад:
const promise = new Promise((resolve, reject) => {
setTimeout(() => resolve("Успіх"), 1000);
});
promise.then((value) => {
console.log("Виконано з результатом:", value);
}).catch((error) => {
console.error("Відхилено з помилкою:", error);
});
Пояснення:
- Функція зворотного виклику onFulfillment реєструється, коли викликається
.then()
. - Як тільки викликається функція
resolve
, зворотний виклик переміщається з масиву onFulfillment в чергу мікротасків. Якщо була викликана функціяreject
, зворотний виклик переміщається з масиву onRejection в чергу мікротасків. - Цикл подій (event loop) обробляє чергу мікротасків перед переходом до наступної макротаски.
Використання Promise: за допомогою .then()
та .catch()
Promise працюють як об'єкти-заповнювачі для операцій, які завершаться в майбутньому. Як тільки асинхронна операція завершиться, ми можемо використовувати .then()
та .catch()
для обробки результатів або помилок.
Що приймає .then()
?
Метод .then()
приймає до двох аргументів:
- onFulfilled (необов'язково): Функція, яка виконується, коли Promise вирішено. Вона отримує значення, що було вирішено, як аргумент.
- onRejected (необов'язково): Функція, яка виконується, коли Promise відхилено. Вона отримує причину відхилення як аргумент.
Приклад:
const promise = Promise.resolve("Розв'язане значення");
promise.then(
(value) => {
// це потрапить у масив onFulfillment
console.log("Обробник виконання викликаний з значенням:", value);
},
(error) => {
// це потрапить у масив onRejection
console.error("Обробник відхилення викликаний з помилкою:", error);
}
);
- Якщо аргумент
onRejected
пропущений, ми можемо використовувати.catch()
для обробки відхилень.
Що повертає .then()
?
Метод .then()
завжди повертає новий Promise. Це дозволяє використовувати ланцюжки Promise.
- Якщо обробник
onFulfilled
абоonRejected
повертає значення, це значення буде обгорнуте в розв'язаний Promise. - Якщо обробник викидає помилку, повернутий Promise буде відхилений з цією помилкою.
- Якщо обробник сам повертає Promise, стан повернутого Promise залежить від цього внутрішнього Promise.
Приклад:
const promise = Promise.resolve(10);
promise
.then((value) => {
console.log("Значення:", value);
return value * 2;
})
.then((value) => {
console.log("Подвоєне значення:", value);
});
Виведення:
Значення:10
Подвоєне значення:20
Що приймає .catch()
?
Метод .catch()
використовується виключно для обробки відхилень Promise. Він приймає один аргумент:
- onRejected: Функція, яка виконується, коли Promise відхилено. Вона отримує причину відхилення як аргумент.
Приклад:
const promise = Promise.reject("Щось пішло не так");
promise.catch((error) => {
console.error("Обробник catch викликаний з помилкою:", error);
});
- Якщо метод
.catch()
використовується після.then()
, він оброблятиме тільки ті відхилення, які сталися в попередньому ланцюгу.then()
.
Що повертає .catch()
?
Як і .then()
, метод .catch()
також повертає новий Promise.
Це дозволяє ланцюжити додаткові виклики .then()
.
- Якщо обробник
onRejected
повертає значення, то повернутий Promise буде виконано з цим значенням. - Якщо обробник
onRejected
викидає помилку, то повернутий Promise буде відхилений з цією помилкою.
Приклад:
const promise = Promise.reject("Початкова помилка");
promise
.catch((error) => {
console.error("Оброблена помилка:", error);
return "Відновлене значення";
})
.then((value) => {
console.log("Відновлене значення:", value);
});
Виведення:
Оброблена помилка:Початкова помилка
Відновлене значення:Відновлене значення
Використання .then()
та .catch()
:
const promise = new Promise((resolve, reject) => {
setTimeout(() => {
resolve("Успіх");
}, 1000);
});
promise
.then((value) => {
console.log("Виконано з результатом:", value);
})
.catch((error) => {
console.error("Відхилено з помилкою:", error);
});
Завдання та пріоритети
Цикл подій JavaScript надає пріоритет черзі мікротасків перед чергою макротасків.
Приклад:
console.log("Старт скрипта");
const promise = Promise.resolve("Розв'язане значення");
promise.then((value) => {
console.log("Promise розв'язано:", value);
});
setTimeout(() => {
console.log("setTimeout callback");
}, 0);
console.log("Кінець скрипта");
Виведення:
Старт скрипта
Кінець скрипта
Promise розв'язано: Розв'язане значення
setTimeout callback
Типові помилки
- Забуття викликати Resolve/Reject:
- Якщо не викликається ні
resolve
, ніreject
, Promise залишається в стані pending (очікує) безкінечно.
2. Блокування циклу подій:
- Уникайте синхронних довготривалих задач в функції-ініціаторі, щоб не блокувати інші операції.
3. Необроблені помилки:
- Завжди використовуйте
.catch()
для обробки відхилень та помилок.
promise.catch((error) => {
console.error("Щось пішло не так:", error);
});
Висновок
Розуміння того, як працюють Promises — зокрема синхронне виконання їх функції-ініціатора і використання черги мікротасків — є важливим для написання ефективного та ненав'язливого JavaScript-коду. Дотримуючись найкращих практик і уникаючи типових помилок, ви зможете повною мірою використовувати потужність Promises для ефективного керування асинхронними операціями.
Перекладено з: Understanding JavaScript Promises: Asynchronous Behavior and Best Practices