Переклад не закінчено. Будь ласка, допоможіть перекласти цю статтю з англійської.

Promise   ( з англ. - "обіцянка", далі - "проміс" )   це об'єкт,   що являє собою можливе завершення або невдачі асинхронної операції.  Проміс може бути створенний за допогою конструктора. Однак, більшість людей є споживачами раніше створенних промісів, що повертаються функціями. Саме тому цей путівник,  в першу чергу, дослідить  споживання промісів, що повертаються.

По суті, проміс це об'єкт, що повертається, до якого ви прикріпляєте колбеки, замість передавання колбеків у функцію.

Напр., У випадку  завершення або невдачі ви викликаєте лише один з  колбеків, замість використання old-style функції , що очікує два колбеки:

function successCallback(result) {
  console.log("It succeeded with " + result);
}

function failureCallback(error) {
  console.log("It failed with " + error);
}

doSomething(successCallback, failureCallback);

… замість цього, сучасні функції повертають проміс, до якого ви можете приєднати ваші колбеки:

let promise = doSomething(); 
promise.then(successCallback, failureCallback);

…чи простіше:

doSomething().then(successCallback, failureCallback);

Ми називаємо це асинхронним викликом функції.  Ця конвенція має декілька переваг. Ми дослідимо кожну з них.

Гарантії

На відміну від застарілих  колбеків, проміс постачається з деякими гарантіями:

  • Колбеки ніколи не будуть викликані до завершення поточного перебігу циклу подій JavaScript.
  • Колбеки, що додані за допомогою .then будуть викликані, як зазначено вище, навіть після завершення або невдачі асинхронної операції.
  •  Кілька колбеків можуть бути додані за домопогою виклику метода .then декілька разів і викликані незалжено в порядку вставки.

Але безпосередньою перевагою промісів є ланцюгові зв'язки.

Ланцюжок

Загальною потребою є виклик двох або більше асинхронних операцій впритул, коли кожна послідовна операція починається, коли попередня завершується, з результатом із попереднього кроку. Ми досягаємо цього за допомогою створення ланцюжка промісів.

Ось вам магія:  функція then повертає новий проміс, що відрізняється від оригінала:

let promise = doSomething();
let promise2 = promise.then(successCallback, failureCallback);

чи

let promise2 = doSomething().then(successCallback, failureCallback);

Цей другий проміс представляє собою завершення не тільки  doSomething() , але й successCallback або failureCallback , що ви передаєте, що в свою чергу можуть бути асинхронними операціями , що повертають проміс. В цьому випадку, будь-який колбек, що додається до  promise2 стає в чергу за промісом, що повертається з successCallback чи failureCallback. .

This second promise represents the completion not just of doSomething(), but also of the successCallback or failureCallback you passed in, which can be other asynchronous functions returning a promise. When that's the case, any callbacks added to promise2 get queued behind the promise returned by either successCallback or failureCallback.

По суті, кожен проміс являє собою заврешення  іншого асинхроннго кроку в ланцюжку.

В старі часи, виконання декількох асинхронних операцій підряд призвело б до класичної піраміди смерті з колбеків:

doSomething(function(result) {
  doSomethingElse(result, function(newResult) {
    doThirdThing(newResult, function(finalResult) {
      console.log('Got the final result: ' + finalResult);
    }, failureCallback);
  }, failureCallback);
}, failureCallback);

Замість цього, з сучасними функціями , ми приєднуємо наші колбеки до промісів, що повертаються, формуючи ланцюжок промісів.

doSomething().then(function(result) {
  return doSomethingElse(result);
})
.then(function(newResult) {
  return doThirdThing(newResult);
})
.then(function(finalResult) {
  console.log('Got the final result: ' + finalResult);
})
.catch(failureCallback);

Аргументи до  then є необов'язковими, а catch(failureCallback)   це короткий запис для  then(null, failureCallback) . Натомість ви можете побачити це, виражене за допомгою стрілочних функцій:

doSomething()
.then(result => doSomethingElse(result))
.then(newResult => doThirdThing(newResult))
.then(finalResult => {
  console.log(`Got the final result: ${finalResult}`);
})
.catch(failureCallback);

Важливо: Завжди повертайте проміси , інакше колбеки не сформують ланцюг і помилки не будуть спіймані ( коли  {} опущені, стрілочні функції повертають неявно ).
Ланцюгування після catch

Chaining after a catch

Ланцюгувати після невдачі можливо, напр.  catch є корисним у разі виконання нових операцій навіть після того, як операція у ланцюжку провалилась. Прочитайте наступний приклад:

new Promise((resolve, reject) => {
    console.log('Initial');

    resolve();
})
.then(() => {
    throw new Error('Something failed');
        
    console.log('Do this');
})
.catch(() => {
    console.log('Do that');
})
.then(() => {
    console.log('Do this whatever happened before');
});

Це виведе  наступний текст:

Initial
Do that
Do this whatever happened before

Зауважте, що текст  "Do this"  не був виведений , тому що  помилка "Something failed" викликала відмову.

Розмноження помилок

Ви, можливо, згадали, як тричи бачили failureCallback  раніше, в піраміді смерті, і тепер порівняйте це лише з одним викликом наприкінці ланцюжку промісів.

doSomething()
.then(result => doSomethingElse(value))
.then(newResult => doThirdThing(newResult))
.then(finalResult => console.log(`Got the final result: ${finalResult}`))
.catch(failureCallback);

По суті, ланцюжок промісів зупиняється , якщо є виключення, натомість видивляючись далі по ланцюжку обробювачі catch.  Це дуже добре моделюється дивлячись на те, як працює синхронний код:

try {
  let result = syncDoSomething();
  let newResult = syncDoSomethingElse(result);
  let finalResult = syncDoThirdThing(newResult);
  console.log(`Got the final result: ${finalResult}`);
} catch(error) {
  failureCallback(error);
}

Ця симметрія з синхронним кодом кульмінує в синтаксичному цукрі async/await в ECMAScript 2017:

async function foo() {
  try {
    let result = await doSomething();
    let newResult = await doSomethingElse(result);
    let finalResult = await doThirdThing(newResult);
    console.log(`Got the final result: ${finalResult}`);
  } catch(error) {
    failureCallback(error);
  }
}

Це побудовано на промісах , напр.  doSomething()   це та сама функція, що й до цього. Ви можете прочитати більше про синтаксис тут.

Проміси вирішують фундаментальний недолік піраміди смерті, ловлячи всі помилки, навіть прокинуті виключення та помилки программування. Це необхідно для функціональної композиції асинхронних операцій.

Створення промісу на основі старого  callback API

Promise може бути створенний з нуля за допогою його конструктора. Це необхідно лише для обгортки старих API.

В ідеальному світі , всі асинхронні функції повертали б проміси. На жаль, деякі API досі очікують передачу колбеків успіху та/або невдачі в старому стилі. Прикладом-квінтесенцією є функція  setTimeout() :

setTimeout(() => saySomething("10 seconds passed"), 10000);

Змішування застарілих коллбеків і промісів є проблематичним. Якщо  saySomething провалиться або буде мати помилку программування, нічого не спіймає це.

На щастя, ми можемо обгорнути це у проміс. Кращою практикою є обгортання проблематичної функції на якомога нижчому рівні, і потім ніколи не звертатись безпосередньо до неї.

const wait = ms => new Promise(resolve => setTimeout(resolve, ms));

wait(10000).then(() => saySomething("10 seconds")).catch(failureCallback);

По суті,  конструктор промісу приймає функцію виконання, що дозволяє вирішити або відхилити  проміс вручну. Так як setTimeout реально ніколи не проваляються, ми опускаємо нашу функцію відмови в цьому випадку.

Композиція

Promise.resolve() та Promise.reject() є короткими записами для створення вручну вже вирішених або відхилених промісів відповідно. Інколи це може бути корисно.

Promise.all() та Promise.race() є двома інтсрументами композиції для виконання асинхронних операції паралельно.

Послідовна композиція можлива з використанням деякого розумного JavaScript:

[func1, func2].reduce((p, f) => p.then(f), Promise.resolve());

По суті, ми зменшуємо масив асинхронних функцій до  ланцюжка промісів, що еквівалентний : Promise.resolve().then(func1).then(func2);

Це також може бути виконано за допомогою здатної до повторного використання функції компонування, що є звичною  у функціональному програмуванні:

let applyAsync = (acc,val) => acc.then(val);
let composeAsync = (...funcs) => x => funcs.reduce(applyAsync, Promise.resolve(x));

Функція composeAsync прийме будь-яку кількість функцій в якості аргументів, а також поверне нову функцію, що приймає  початкове значення, що має пройти крізь трубопровід композиції. Це є перевагою, так як, незважаючи на те, що  функції можуть бути як синхронними так і асинхронними, вони гарантовано виконуються у правильній послідовності:

let transformData = composeAsync(func1, asyncFunc1, asyncFunc2, func2);
transformData(data);

В ECMAScript 2017, послідовна композиція може бути створена просто за допомогою async/await:

for (let f of [func1, func2]) {
  await f();
}

Хронометраж

Задля того, щоб уникнути сюрпризів, функції, що передані до  then ніколи не будуть викликані синхронно, навіть якщо присутній вже вирішенний проміс:

Promise.resolve().then(() => console.log(2));
console.log(1); // 1, 2

Функції, що передаються, замість того, щоб бути виконанними негайно,  стають у чергу мікрозадач, що означає, що вони виконуються пізніше, коли черга спустошується в кінці поточного виконання циклу подій JavaScript , тобто досить скоро:

const wait = ms => new Promise(resolve => setTimeout(resolve, ms));

wait().then(() => console.log(4));
Promise.resolve().then(() => console.log(2)).then(() => console.log(3));
console.log(1); // 1, 2, 3, 4

Дивіться також

Мітки документа й учасники

 Зробили внесок у цю сторінку: AVorona
 Востаннє оновлена: AVorona,