Обʼєкт Promise представляє майбутнє завершення (або невдачу) асинхронної операції та її результуюче значення.
Це складна тема для багатьох людей, особливо якщо ми знаємо програмування в мові, яка повністю синхронна. Якщо ми відчуваємо, що це занадто складно, або хочемо дізнатися більше про конкурентність і паралелізм, подивімося (через go.dev) або подивімося безпосередньо через vimeo і прочитаймо слайди блискучої доповіді «Concurrency is not parallelism».
Promise має три стани:
Коли проміс створюють, він перебуває в стані очікування. У якийсь момент у майбутньому він може виконатися або відхилитися. Щойно проміс виконався або відхилився, він уже ніколи не зможе виконатися чи відхилитися знову, і його стан теж не може змінитися.
Іншими словами:
Проміс можна виконати в різні способи:
// Creates a promise that is immediately resolved
Promise.resolve(value);
// Creates a promise that is immediately resolved
new Promise((resolve) => {
resolve(value);
});
// Chaining a promise leads to a resolved promise
somePromise.then(() => {
// ...
return value;
});
У наведених прикладах value може бути чим завгодно, зокрема помилкою, undefined, null або іншим промісом.
Зазвичай ми хочемо виконати проміс зі значенням, яке не є помилкою.
Проміс можна відхилити в різні способи:
// Creates a promise that is immediately rejected
Promise.reject(reason)
// Creates a promise that is immediately rejected
new Promise((_, reject) {
reject(reason)
})
// Chaining a promise with an error leads to a rejected promise
somePromise.then(() => {
// ...
throw reason
})
У наведених прикладах reason може бути чим завгодно, зокрема помилкою, undefined або null.
Зазвичай ми хочемо відхилити проміс із помилкою.
Проміс можна продовжити майбутньою дією, щойно він виконається або відхилиться.
promise.then() викликається, коли promise виконуєтьсяpromise.catch() викликається, коли promise відхиляєтьсяpromise.finally() викликається, коли promise або виконується, або відхиляєтьсяКожен проміс є «thenable».
Це означає, що в нього є доступна функція then, яка виконається, щойно початковий проміс виконається.
Якщо маємо promise.then(onResolved), то функція зворотного виклику onResolved отримує значення, яким було виконано початковий проміс.
Це завжди повертає новий проміс у ланцюжку.
Повернення value з then виконує проміс у ланцюжку.
Якщо в then кинути reason, проміс у ланцюжку відхиляється.
const promise1 = new Promise(function (resolve, reject) {
setTimeout(() => {
resolve('Success!');
}, 1000);
});
const promise2 = promise1.then(function (value) {
console.log(value);
// expected output: "Success!"
return true;
});
Це виведе "Success!" приблизно через 1000 мс.
Стан і значення promise1 будуть resolved і "Success!".
Стан і значення promise2 будуть resolved і true.
Є й другий аргумент, який запускається, коли початковий проміс відхиляється.
Якщо маємо promise.then(onResolved, onRejected), то функція зворотного виклику onResolved отримує значення, яким було виконано початковий проміс, або функція зворотного виклику onRejected отримує причину, з якої проміс було відхилено.
const promise1 = new Promise(function (resolve, reject) {
setTimeout(() => {
resolve('Success!');
}, 1000);
if (Math.random() < 0.5) {
reject('Nope!');
}
});
function log(value) {
console.log(value);
return true;
}
function shout(reason) {
console.error(reason.toUpperCase());
return false;
}
const promise2 = promise1.then(log, shout);
"Success!" приблизно через 1000 мс.
promise1 будуть resolved і "Success!".promise2 будуть resolved і true."NOPE!".
promise1 будуть rejected і Nope!.promise2 будуть resolved і false.Важливо розуміти, що через правила життєвого циклу, коли відбувається reject, той resolve, який надходить приблизно через 1000 мс, мовчки ігнорується, бо внутрішній стан не може змінитися після відхилення чи виконання.
Важливо розуміти, що повернення значення з проміса виконує його, а викидання значення відхиляє його.
Коли promise1 виконується і є приєднаний onResolved: then(onResolved), то цей наступний крок стає новим промісом, який може виконатися або відхилитися.
Коли promise1 відхиляється, але є приєднаний onRejected: then(, onRejected), то цей наступний крок стає новим промісом, який може виконатися або відхилитися.
Іноді ми хочемо перехопити помилки й продовжити лише тоді, коли початковий проміс зазнає reject.
Якщо маємо promise.catch(onCatch), то функція зворотного виклику onCatch отримує причину, з якої початковий проміс було відхилено.
Це завжди повертає новий проміс у ланцюжку.
Повернення value з catch виконує проміс у ланцюжку.
Якщо в catch кинути reason, проміс у ланцюжку відхиляється.
const promise1 = new Promise(function (resolve, reject) {
setTimeout(() => {
resolve('Success!');
}, 1000);
if (Math.random() < 0.5) {
reject('Nope!');
}
});
function log(value) {
console.log(value);
return 'done';
}
function recover(reason) {
console.error(reason.toUpperCase());
return 42;
}
const promise2 = promise1.catch(recover).then(log);
Приблизно в половині випадків це виведе "Success!" приблизно через 1000 мс.
В іншій половині випадків це одразу виведе 42.
promise1 виконується, catch пропускається, керування доходить до then і значення виводиться в консоль.
promise1 будуть resolved і "Success!".promise2 будуть resolved і "done";promise1 відхиляється, виконується catch, який повертає значення, і таким чином ланцюжок тепер перебуває в стані resolved, керування доходить до then і значення виводиться в консоль.
promise1 будуть rejected і "Nope!".promise2 будуть resolved і "done";Іноді ми хочемо виконати код після того, як проміс завершиться, незалежно від того, виконався він чи відхилився.
Якщо маємо promise.finally(onSettled), то функція зворотного виклику onSettled не отримує нічого.
Це завжди повертає новий проміс у ланцюжку.
Повернення value з finally копіює стан і значення початкового проміса, ігноруючи value.
Якщо в finally кинути reason, проміс у ланцюжку відхиляється, перезаписуючи будь-який стан і значення чи причину початкового проміса.
Кілька методів разом:
const myPromise = new Promise(function (resolve, reject) {
const sampleData = [2, 4, 6, 8];
const randomNumber = Math.round(Math.random() * 5);
if (sampleData[randomNumber]) {
resolve(sampleData[randomNumber]);
} else {
reject('Sampling did not result in a sample');
}
});
const finalPromise = myPromise
.then(function (sampled) {
// If the random number was 0, 1, 2, or 3, this will be
// reached and the number 2, 4, 6, or 8 will be logged.
console.log(`Sampled data: ${sampled}`);
return 'yay';
})
.catch(function (reason) {
// If the random number was 4 or 5, this will be reached and
// reason will be "An error occurred". The entire chain will
// then reject with an Error with the reason as message.
throw new Error(reason);
})
.finally(function () {
// This will always log after either the sampled data is
// logged or the error is raised.
console.log('Promise completed');
});
randomNumber дорівнює 0-3:
myPromise буде виконано зі значенням 2, 4, 6, or 8
finalPromise буде виконано зі значенням 'yay'
Sampled data: ...Promise completedrandomNumber дорівнює 4-5:
myPromise буде відхилено з причиною 'Sampling did not result in a sample'
finalPromise буде відхилено з причиною Error('Sampling did not result in a sample')
Promise completed"uncaught rejected promise: Error('Sampling did not result in a sample')"
Як показано вище, reject працює з рядком тексту (англ. string), і проміс також може відхилитися з Error.
Якщо ланцюжки промісів або загальне використання залишаються незрозумілими, варто звернутися до туторіалу на MDN.
У цій вправі ми створимо TranslationService, який надає базові послуги перекладу безплатним учасникам, а розширений переклад із гарантіями якості - преміум-учасникам.
API
Ми знайшли космічний API перекладу, який виконує будь-який request перекладу за прийнятний час.
І ми хочемо цим скористатися.
Космічні перекладачі вкрай примхливі й не терплять надмірності, тож вони також надають супутники сховища API, де можна fetch минулі переклади, не турбуючи їх.
Отримання перекладу
api.fetch(text) отримує переклад text зі сховища API і повертає promise, який надає два значення:
translation: власне перекладquality: якість, виражена числомЯкщо перекладу немає в сховищі API, API викидає помилку NotAvailable.
Переклади можна додавати за допомогою методу api.request.
Якщо «text» перекласти неможливо, API викидає помилку Untranslatable.
api.fetch('jIyaj');
// => Promise({ resolved: 'I understand' })
Запит перекладу
Деякі переклади точно існують, але їх ще не додано до сховища API. Ось у чому різниця між NotAvailable (немає у сховищі, але можна запросити) і Untranslatable (перекласти неможливо).
api.request(text, callback) запитує виконання перекладу text і додавання його до сховища API.
Після завершення викликається функція callback.
callback отримує undefined: це означає, що переклад вдався і доступний через метод api.fetch.callback отримує error: це означає, що щось пішло не так.
Космічний API нестабільний, тобто часто дає збої.
Якщо це сталося, можна знову викликати api.request.api.request('majQa’', callback);
// => undefined
//
// later: the passed callback is called with undefined
// because it was successful.
⚠ Увага! ⚠
API творить своє чаклунство, телепортуючи різних перекладачів, коли надходить request.
Це дуже дорога операція, тож не варто викликати її, коли переклад уже доступний.
На жаль, не всі читають посібник, тому існує система, яка виганяє недобросовісних користувачів.
Якщо викликати api.request для text, який уже доступний, API викидає AbusiveClientError для цього виклику, і для кожного наступного.
Подбайте про те, щоб ніколи не запитувати переклад, якщо щось уже перекладено.
Безплатна служба надає лише ті переклади, які вже є в сховищі API.
Реалізуйте метод free(text), який надає безплатним учасникам переклади, що вже є в сховищі API.
Ігноруйте якість і передавайте далі будь-які помилки, які викидає API.
api.fetch (api.fetch повертає promise)service.free('jIyaj');
// => Promise<...> resolves "I understand."
service.free("jIyajbe'");
// => Promise<...> rejects Error("Not yet translated")
Реалізуйте метод batch([text, text, ...]) для безплатних учасників, який перекладає масив текстів за допомогою безплатної служби й повертає або всі переклади, або одну помилку.
BatchIsEmpty, якщо не надано жодного текстуservice.batch(['jIyaj', "majQa'"]);
// => Promise<...> resolves ["I understand.", "Well done!"]
service.batch(['jIyaj', "jIyajbe'"]);
// => Promise<...> rejects new Error("Not yet translated")
service.batch([]);
// => Promise<...> rejects BatchIsEmpty()
Реалізуйте метод request(text) для преміум-учасників, який запитує додавання перекладу до сховища API.
Запит має автоматично повторюватися в разі невдачі.
Він має виконати не більше ніж 3 виклики для одного запиту (не дратуйте космічних перекладачів!!!).
api.request не повертає помилку, завершуйте успішно з undefined
api.request повертає помилку, повторіть щонайбільше двічіservice.request("jIyajbe'");
// => Promise<...> resolves (with nothing), can now be retrieved using the fetch API
Реалізуйте метод premium(text, quality) для преміум-учасників, щоб отримувати переклад.
Якщо переклад NotAvailable, запитайте переклад і отримайте його після додавання до сховища API.
Метод має повертати переклад лише тоді, коли він відповідає певному порогу quality.
api.fetch завершується успішно, перевірте якість перед поверненням результатуapi.fetch відхиляється, натомість запитайте перекладapi.request відхиляється, передайте помилку даліservice.premium("jIyajbe'", 100);
// => Promise<...> resolves "I don't understand."
service.premium("'arlogh Qoylu'pu'?", 100);
// => Promise<...> rejects QualityThresholdNotMet()
service.premium("'arlogh Qoylu'pu'?", 40);
// => Promise<...> resolves "What time is it?"
Зверніть увагу.
Правильний переклад 'arlogh Qoylu'pu'? - Скільки разів це чули?.
Зареєструйтеся на Exercism, щоб вивчати й опановувати JavaScript, а також 37 концепцій159 вправ та справжнє наставництво від людей, і все це безкоштовно.